<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://docs.moodle.org/dev/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Nmagill</id>
	<title>MoodleDocs - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://docs.moodle.org/dev/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Nmagill"/>
	<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/Special:Contributions/Nmagill"/>
	<updated>2026-11-03T05:51:12Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.43.5</generator>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User:Neill_Magill&amp;diff=61651</id>
		<title>User:Neill Magill</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User:Neill_Magill&amp;diff=61651"/>
		<updated>2022-01-25T08:45:18Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: Created page with &amp;quot;I have been working with Moodle since 2.2 when the University of Nottingham adopted it as their main VLE.  I have several patches that have made their way into Moodle core (mo...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;I have been working with Moodle since 2.2 when the University of Nottingham adopted it as their main VLE.&lt;br /&gt;
&lt;br /&gt;
I have several patches that have made their way into Moodle core (mostly related to improving performance) and look after numerous plugins that have been developed for the University.&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Activity_completion_API&amp;diff=61641</id>
		<title>Activity completion API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Activity_completion_API&amp;diff=61641"/>
		<updated>2022-01-24T14:38:13Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Add information about the completion settings to the activities cm_info object */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle_2.0}}&lt;br /&gt;
{{Note|There are changes to the completion API introduced in &#039;&#039;&#039;Moodle 3.11&#039;&#039;&#039; to be incorporated to this page. Please refer to [[Student activity completion]] for details.}}&lt;br /&gt;
Modules do not need to be changed to support conditional availability, but they do need changing to support the completion system. &lt;br /&gt;
&lt;br /&gt;
If you make no changes to a module whatsoever, it can only support &#039;manual&#039; completion (where the user ticks a box).&lt;br /&gt;
== Feature support ==&lt;br /&gt;
To support the completion system, your module must include a &#039;&#039;modulename&#039;&#039;&amp;lt;tt&amp;gt;_supports&amp;lt;/tt&amp;gt; function in its &amp;lt;tt&amp;gt;lib.php&amp;lt;/tt&amp;gt;. Here is an example:&lt;br /&gt;
 /**&lt;br /&gt;
  * Indicates API features that the forum supports.&lt;br /&gt;
  *&lt;br /&gt;
  * @param string $feature&lt;br /&gt;
  * @return mixed True if yes (some features may use other values)&lt;br /&gt;
  */&lt;br /&gt;
 function forum_supports($feature) {&lt;br /&gt;
     switch($feature) {&lt;br /&gt;
         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;&lt;br /&gt;
         case FEATURE_COMPLETION_HAS_RULES: return true;&lt;br /&gt;
         default: return null;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
The relevant features for completion are:&lt;br /&gt;
* &#039;&#039;&#039;FEATURE_COMPLETION_TRACKS_VIEWS&#039;&#039;&#039; - the module can support completion &#039;on view&#039;, meaning that an activity becomes marked complete as soon as a user clicks on it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FEATURE_GRADE_HAS_GRADE&#039;&#039;&#039; - the module provides (or may provide, depending on settings) a grade for students. When a module supports grades, it can support completion &#039;on grade&#039;, meaning that an activity becomes marked complete as soon as a user is assigned a grade.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FEATURE_COMPLETION_HAS_RULES&#039;&#039;&#039; - the module has custom completion rules.&lt;br /&gt;
== Completion on view ==&lt;br /&gt;
Completion on view means that, if selected, an activity is marked complete as soon as the user views it. &#039;View&#039; is usually defined as seeing the module&#039;s main page; if you click on the activity, and there isn&#039;t an error, you have probably viewed it. However it is up to each module precisely how they define &#039;view&#039;.&lt;br /&gt;
=== How to implement ===&lt;br /&gt;
In your module&#039;s &#039;&#039;modulename&#039;&#039;&amp;lt;tt&amp;gt;_supports&amp;lt;/tt&amp;gt; function, return true for FEATURE_COMPLETION_TRACKS_VIEWS.&lt;br /&gt;
&lt;br /&gt;
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&#039;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.&lt;br /&gt;
 $completion = new completion_info($course);&lt;br /&gt;
 $completion-&amp;gt;set_module_viewed($cm);&lt;br /&gt;
=== Performance issues ===&lt;br /&gt;
Calling this method has no significant performance cost if &#039;on view&#039; completion is not enabled for the activity. If it is enabled, then the performance cost is kept low because the &#039;viewed&#039; state is cached; it doesn&#039;t add a database query to every request.&lt;br /&gt;
== Completion on grade ==&lt;br /&gt;
Completion on grade means that, if selected, an activity is marked complete as soon as the user receives a grade from that activity.&lt;br /&gt;
=== How to implement ===&lt;br /&gt;
In your module&#039;s _supports function, return true for FEATURE_GRADE_HAS_GRADE. No other action is necessary.&lt;br /&gt;
=== Performance issues ===&lt;br /&gt;
When &#039;on grade&#039; 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.&lt;br /&gt;
== Custom completion rules ==&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
It is a lot harder to implement custom completion rules than it is to use the system-provided &#039;view&#039; or &#039;grade&#039; conditions, but the instructions below should help make it clear.&lt;br /&gt;
=== Implementation overview ===&lt;br /&gt;
To implement custom completion rules, you need to:&lt;br /&gt;
# Return true for FEATURE_COMPLETION_HAS_RULES in your module&#039;s _supports function.&lt;br /&gt;
# Add database fields to your module&#039;s main table to store the custom completion settings.&lt;br /&gt;
# Add backup and restore code to back up these fields.&lt;br /&gt;
# Add information about the completion settings to the activities cm_info object&lt;br /&gt;
# Add controls to your module&#039;s settings form so that users can select the custom rules, altering these database settings.&lt;br /&gt;
# Add a function that checks the value of these rules (if set).&lt;br /&gt;
# Add function returns descriptions for the completion states&lt;br /&gt;
# Add code so that whenever the value affecting a rule might change, you inform the completion system.&lt;br /&gt;
=== Database fields for completion settings ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
Usually the best place to store this information is your module&#039;s main table because:&lt;br /&gt;
* 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.&lt;br /&gt;
* You already read this row with most requests, so there is no need for additional database queries which would reduce performance.&lt;br /&gt;
* The main table is used for most other module options so it is a logical place for this information.&lt;br /&gt;
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.&lt;br /&gt;
==== Example ====&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
The forum adds this field to store a completion option:&lt;br /&gt;
* &#039;&#039;&#039;completionposts&#039;&#039;&#039; - this may be 0 or an integer. If it&#039;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 &#039;completed&#039;.&lt;br /&gt;
=== Backup and restore for completion fields ===&lt;br /&gt;
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. &lt;br /&gt;
&lt;br /&gt;
Remember that your restore code should handle the case when these fields are not present, setting the fields to a suitable default value.&lt;br /&gt;
==== Example ====&lt;br /&gt;
The following code in &amp;lt;tt&amp;gt;backup_forum_stepslib.php&amp;lt;/tt&amp;gt; lists the fields to back up:&lt;br /&gt;
  $forum = new backup_nested_element(&#039;forum&#039;, array(&#039;id&#039;), array(&lt;br /&gt;
            &#039;type&#039;, &#039;name&#039;, &#039;intro&#039;, &#039;introformat&#039;,&lt;br /&gt;
            &#039;assessed&#039;, &#039;assesstimestart&#039;, &#039;assesstimefinish&#039;, &#039;scale&#039;,&lt;br /&gt;
            &#039;maxbytes&#039;, &#039;maxattachments&#039;, &#039;forcesubscribe&#039;, &#039;trackingtype&#039;,&lt;br /&gt;
            &#039;rsstype&#039;, &#039;rssarticles&#039;, &#039;timemodified&#039;, &#039;warnafter&#039;,&lt;br /&gt;
            &#039;blockafter&#039;, &#039;blockperiod&#039;, &#039;completiondiscussions&#039;, &#039;completionreplies&#039;,&lt;br /&gt;
            &#039;completionposts&#039;));&lt;br /&gt;
As you can see, I added the &#039;&#039;&#039;completionposts&#039;&#039;&#039; field (and the others that aren&#039;t covered in this example) to the list of fields.&lt;br /&gt;
=== Add information about the completion settings to the activities cm_info object ===&lt;br /&gt;
{{Moodle 3.3}}You will need to add information about the custom rules into the activities cm_info object by either adding, or modifying the module_get_coursemodule_info callback&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Add a get_coursemodule_info function in case any forum type wants to add &#039;extra&#039; information&lt;br /&gt;
 * for the course (see resource).&lt;br /&gt;
 *&lt;br /&gt;
 * Given a course_module object, this function returns any &amp;quot;extra&amp;quot; information that may be needed&lt;br /&gt;
 * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.&lt;br /&gt;
 *&lt;br /&gt;
 * @param stdClass $coursemodule The coursemodule object (record).&lt;br /&gt;
 * @return cached_cm_info An object on information that the courses&lt;br /&gt;
 *                        will know about (most noticeably, an icon).&lt;br /&gt;
 */&lt;br /&gt;
function forum_get_coursemodule_info($coursemodule) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    $dbparams = [&#039;id&#039; =&amp;gt; $coursemodule-&amp;gt;instance];&lt;br /&gt;
    $fields = &#039;id, name, intro, introformat, completionposts, completiondiscussions, completionreplies, duedate, cutoffdate&#039;;&lt;br /&gt;
    if (!$forum = $DB-&amp;gt;get_record(&#039;forum&#039;, $dbparams, $fields)) {&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $result = new cached_cm_info();&lt;br /&gt;
    $result-&amp;gt;name = $forum-&amp;gt;name;&lt;br /&gt;
&lt;br /&gt;
    if ($coursemodule-&amp;gt;showdescription) {&lt;br /&gt;
        // Convert intro to html. Do not filter cached version, filters run at display time.&lt;br /&gt;
        $result-&amp;gt;content = format_module_intro(&#039;forum&#039;, $forum, $coursemodule-&amp;gt;id, false);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Populate the custom completion rules as key =&amp;gt; value pairs, but only if the completion mode is &#039;automatic&#039;.&lt;br /&gt;
    if ($coursemodule-&amp;gt;completion == COMPLETION_TRACKING_AUTOMATIC) {&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;customcompletionrules&#039;][&#039;completiondiscussions&#039;] = $forum-&amp;gt;completiondiscussions;&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;customcompletionrules&#039;][&#039;completionreplies&#039;] = $forum-&amp;gt;completionreplies;&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;customcompletionrules&#039;][&#039;completionposts&#039;] = $forum-&amp;gt;completionposts;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Populate some other values that can be used in calendar or on dashboard.&lt;br /&gt;
    if ($forum-&amp;gt;duedate) {&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;duedate&#039;] = $forum-&amp;gt;duedate;&lt;br /&gt;
    }&lt;br /&gt;
    if ($forum-&amp;gt;cutoffdate) {&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;cutoffdate&#039;] = $forum-&amp;gt;cutoffdate;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return $result;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Form changes for completion settings ===&lt;br /&gt;
When you have custom completion conditions, you need to add controls to your module&#039;s settings form &amp;lt;tt&amp;gt;mod_form.php&amp;lt;/tt&amp;gt; so that users can select these conditions. You can add any necessary controls.&lt;br /&gt;
* Implement the &amp;lt;tt&amp;gt;add_completion_rules&amp;lt;/tt&amp;gt; function which adds the form controls for your new rules.&lt;br /&gt;
&lt;br /&gt;
* Implement the &amp;lt;tt&amp;gt;completion_rule_enabled&amp;lt;/tt&amp;gt; function which is called during form validation to check whether one of your module&#039;s completion rules has been selected.&lt;br /&gt;
&lt;br /&gt;
* 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.&lt;br /&gt;
==== Example ====&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
First, the function that adds these controls:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Add elements for setting the custom completion rules.&lt;br /&gt;
 *  &lt;br /&gt;
 * @category completion&lt;br /&gt;
 * @return array List of added element names, or names of wrapping group elements.&lt;br /&gt;
 */&lt;br /&gt;
public function add_completion_rules() {&lt;br /&gt;
&lt;br /&gt;
    $mform = $this-&amp;gt;_form;&lt;br /&gt;
&lt;br /&gt;
    $group = [&lt;br /&gt;
        $mform-&amp;gt;createElement(&#039;checkbox&#039;, &#039;completionpostsenabled&#039;, &#039; &#039;, get_string(&#039;completionposts&#039;, &#039;forum&#039;)),&lt;br /&gt;
        $mform-&amp;gt;createElement(&#039;text&#039;, &#039;completionposts&#039;, &#039; &#039;, [&#039;size&#039; =&amp;gt; 3]),&lt;br /&gt;
    ];&lt;br /&gt;
    $mform-&amp;gt;setType(&#039;completionposts&#039;, PARAM_INT);&lt;br /&gt;
    $mform-&amp;gt;addGroup($group, &#039;completionpostsgroup&#039;, get_string(&#039;completionpostsgroup&#039;,&#039;forum&#039;), [&#039; &#039;], false);&lt;br /&gt;
    $mform-&amp;gt;addHelpButton(&#039;completionpostsgroup&#039;, &#039;completionposts&#039;, &#039;forum&#039;);&lt;br /&gt;
    $mform-&amp;gt;disabledIf(&#039;completionposts&#039;, &#039;completionpostsenabled&#039;, &#039;notchecked&#039;);&lt;br /&gt;
&lt;br /&gt;
    return [&#039;completionpostsgroup&#039;];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* The function creates a checkbox and a text input field, which is set to accept only numbers.&lt;br /&gt;
* These are grouped together so they appear on the same line, and we add a help button.&lt;br /&gt;
* The text input field is disabled if the checkbox isn&#039;t ticked.&lt;br /&gt;
* 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.)&lt;br /&gt;
** This is used so that your controls become disabled if automatic completion is not selected.&lt;br /&gt;
Next, a function for checking whether the user selected this option:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Called during validation to see whether some module-specific completion rules are selected.&lt;br /&gt;
 *&lt;br /&gt;
 * @param array $data Input data not yet validated.&lt;br /&gt;
 * @return bool True if one or more rules is enabled, false if none are.&lt;br /&gt;
 */&lt;br /&gt;
public function completion_rule_enabled($data) {&lt;br /&gt;
    return (!empty($data[&#039;completionpostsenabled&#039;]) &amp;amp;&amp;amp; $data[&#039;completionposts&#039;] != 0);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* The custom completion rule is enabled if the &#039;enabled&#039; checkbox is ticked and the text field value is something other than zero.&lt;br /&gt;
** This is used to give an error if the user selects automatic completion, but fails to select any conditions.&lt;br /&gt;
That&#039;s all the &#039;required&#039; 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).&lt;br /&gt;
 function get_data() {&lt;br /&gt;
     $data = parent::get_data();&lt;br /&gt;
     if (!$data) {&lt;br /&gt;
         return $data;&lt;br /&gt;
     }&lt;br /&gt;
     if (!empty($data-&amp;gt;completionunlocked)) {&lt;br /&gt;
         // Turn off completion settings if the checkboxes aren&#039;t ticked&lt;br /&gt;
         $autocompletion = !empty($data-&amp;gt;completion) &amp;amp;&amp;amp; $data-&amp;gt;completion==COMPLETION_TRACKING_AUTOMATIC;&lt;br /&gt;
         if (empty($data-&amp;gt;completionpostsenabled) || !$autocompletion) {&lt;br /&gt;
            $data-&amp;gt;completionposts = 0;&lt;br /&gt;
         }&lt;br /&gt;
     }&lt;br /&gt;
     return $data;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
You may have noticed the &#039;completionunlocked&#039; check. When some users have already completed the activity, the completion settings are &#039;locked&#039;; they are disabled and cannot be edited, so there will be no value set for those fields in the &amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;$data&amp;lt;/syntaxhighlight&amp;gt; object. Normally this will automatically work but when dealing with checkboxes you need to include a check for the &#039;completionunlocked&#039; value before doing anything that would cause one of those fields to be changed in the database.&lt;br /&gt;
Finally, forum already had a &amp;lt;tt&amp;gt;data_preprocessing&amp;lt;/tt&amp;gt; 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:&lt;br /&gt;
 function data_preprocessing(&amp;amp;$default_values){&lt;br /&gt;
     // [Existing code, not shown]&lt;br /&gt;
 &lt;br /&gt;
     // Set up the completion checkboxes which aren&#039;t part of standard data.&lt;br /&gt;
     // We also make the default value (if you turn on the checkbox) for those&lt;br /&gt;
     // numbers to be 1, this will not apply unless checkbox is ticked.&lt;br /&gt;
     $default_values[&#039;completionpostsenabled&#039;]=&lt;br /&gt;
         !empty($default_values[&#039;completionposts&#039;]) ? 1 : 0;&lt;br /&gt;
     if(empty($default_values[&#039;completionposts&#039;])) {&lt;br /&gt;
         $default_values[&#039;completionposts&#039;]=1;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
Phew! That&#039;s the form done.&lt;br /&gt;
=== Completion state function ===&lt;br /&gt;
When you create completion conditions, you need to write a function &#039;&#039;module&#039;&#039;&amp;lt;tt&amp;gt;_get_completion_state&amp;lt;/tt&amp;gt; that checks the value of those conditions for a particular user. &lt;br /&gt;
&lt;br /&gt;
The function receives as parameters &amp;lt;tt&amp;gt;$course&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;$cm&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;$userid&amp;lt;/tt&amp;gt; - all self-explanatory, I hope - and &amp;lt;tt&amp;gt;$type&amp;lt;/tt&amp;gt;. This has two values: &lt;br /&gt;
* &#039;&#039;&#039;COMPLETION_AND&#039;&#039;&#039; - if multiple conditions are selected, the user must meet all of them.&lt;br /&gt;
* &#039;&#039;&#039;COMPLETION_OR&#039;&#039;&#039; (not currently used) - if multiple conditions are selected, any one of them is good enough to complete the activity.&lt;br /&gt;
Your function should return:&lt;br /&gt;
* &#039;&#039;&#039;true&#039;&#039;&#039; if your custom completion options are enabled and the user meets the conditions.&lt;br /&gt;
* &#039;&#039;&#039;false&#039;&#039;&#039; if your custom completion options are enabled but the user does not yet meet the conditions.&lt;br /&gt;
* &amp;lt;tt&amp;gt;$type&amp;lt;/tt&amp;gt; (not false!) if none of your custom completion options are not enabled.&lt;br /&gt;
==== Example ====&lt;br /&gt;
Here&#039;s the function for forum (simplified to include only the one completion option):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
 /**&lt;br /&gt;
  * Obtains the automatic completion state for this forum based on any conditions&lt;br /&gt;
  * in forum settings.&lt;br /&gt;
  *&lt;br /&gt;
  * @param object $course Course&lt;br /&gt;
  * @param object $cm Course-module&lt;br /&gt;
  * @param int $userid User ID&lt;br /&gt;
  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)&lt;br /&gt;
  * @return bool True if completed, false if not, $type if conditions not set.&lt;br /&gt;
  */&lt;br /&gt;
 function forum_get_completion_state($course,$cm,$userid,$type) {&lt;br /&gt;
     global $CFG,$DB;&lt;br /&gt;
 &lt;br /&gt;
     // Get forum details&lt;br /&gt;
     $forum = $DB-&amp;gt;get_record(&#039;forum&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance), &#039;*&#039;, MUST_EXIST);&lt;br /&gt;
 &lt;br /&gt;
     // If completion option is enabled, evaluate it and return true/false &lt;br /&gt;
     if($forum-&amp;gt;completionposts) {&lt;br /&gt;
         return $forum-&amp;gt;completionposts &amp;lt;= $DB-&amp;gt;get_field_sql(&amp;quot;&lt;br /&gt;
 SELECT &lt;br /&gt;
     COUNT(1) &lt;br /&gt;
 FROM &lt;br /&gt;
     {forum_posts} fp &lt;br /&gt;
     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id&lt;br /&gt;
 WHERE&lt;br /&gt;
     fp.userid=:userid AND fd.forum=:forumid&amp;quot;,&lt;br /&gt;
             array(&#039;userid&#039;=&amp;gt;$userid,&#039;forumid&#039;=&amp;gt;$forum-&amp;gt;id));&lt;br /&gt;
     } else {&lt;br /&gt;
         // Completion option is not enabled so just return $type&lt;br /&gt;
         return $type;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Add function returns descriptions for the completion states ===&lt;br /&gt;
{{Moodle 3.3}}When you create completion conditions, you need to write a function &#039;&#039;module&#039;&#039;&amp;lt;tt&amp;gt;_get_completion_active_rule_descriptions&amp;lt;/tt&amp;gt; that gives a human readable description of the completion state.&lt;br /&gt;
&lt;br /&gt;
The input for the method is the cm_info object for that activity.&lt;br /&gt;
&lt;br /&gt;
You need to return an array of strings for each completion rule that is active.&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Callback which returns human-readable strings describing the active completion custom rules for the module instance.&lt;br /&gt;
 *&lt;br /&gt;
 * @param cm_info|stdClass $cm object with fields -&amp;gt;completion and -&amp;gt;customdata[&#039;customcompletionrules&#039;]&lt;br /&gt;
 * @return array $descriptions the array of descriptions for the custom rules.&lt;br /&gt;
 */&lt;br /&gt;
function mod_forum_get_completion_active_rule_descriptions($cm) {&lt;br /&gt;
    // Values will be present in cm_info, and we assume these are up to date.&lt;br /&gt;
    if (empty($cm-&amp;gt;customdata[&#039;customcompletionrules&#039;])&lt;br /&gt;
        || $cm-&amp;gt;completion != COMPLETION_TRACKING_AUTOMATIC) {&lt;br /&gt;
        return [];&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $descriptions = [];&lt;br /&gt;
    foreach ($cm-&amp;gt;customdata[&#039;customcompletionrules&#039;] as $key =&amp;gt; $val) {&lt;br /&gt;
        switch ($key) {&lt;br /&gt;
            case &#039;completiondiscussions&#039;:&lt;br /&gt;
                if (!empty($val)) {&lt;br /&gt;
                    $descriptions[] = get_string(&#039;completiondiscussionsdesc&#039;, &#039;forum&#039;, $val);&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            case &#039;completionreplies&#039;:&lt;br /&gt;
                if (!empty($val)) {&lt;br /&gt;
                    $descriptions[] = get_string(&#039;completionrepliesdesc&#039;, &#039;forum&#039;, $val);&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            case &#039;completionposts&#039;:&lt;br /&gt;
                if (!empty($val)) {&lt;br /&gt;
                    $descriptions[] = get_string(&#039;completionpostsdesc&#039;, &#039;forum&#039;, $val);&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            default:&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return $descriptions;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Notifying the completion system ===&lt;br /&gt;
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. &lt;br /&gt;
* To ensure performance is not compromised, you should notify the system only when the completion state might actually have changed. Don&#039;t notify the system unless your custom completion rule is actually enabled.&lt;br /&gt;
* You need to pass in the &#039;possible result&#039; of the change. This is used to significantly improve performance. There are three values:&lt;br /&gt;
**&#039;&#039;&#039;COMPLETION_COMPLETE&#039;&#039;&#039; - this change will either have no effect on the user&#039;s completion state, or it will make it complete. The change cannot make a user&#039;s state &#039;&#039;in&#039;&#039;complete if it was complete previously. In the forum example, when you add a post, there is no way this can make the user&#039;s state incomplete, so this possible result applies.&lt;br /&gt;
** &#039;&#039;&#039;COMPLETION_INCOMPLETE&#039;&#039;&#039; - this change will either have no effect on the user&#039;s completion state, or it will make it incomplete. The change cannot make a user&#039;s state complete if it was incomplete previously. Deleting a forum post would fall into this category.&lt;br /&gt;
** &#039;&#039;&#039;COMPLETION_UNKNOWN&#039;&#039;&#039; - 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.&lt;br /&gt;
* If the user whose completion state would be updated is not the current user, then the optional &amp;lt;tt&amp;gt;$userid&amp;lt;/tt&amp;gt; parameter must be included. For example, if a teacher deletes a student&#039;s forum post, then it is the student&#039;s completion state which may need updating, not the teacher&#039;s.&lt;br /&gt;
==== Example ====&lt;br /&gt;
Here&#039;s the code that runs when somebody makes a new forum post:&lt;br /&gt;
 // Update completion state&lt;br /&gt;
 $completion=new completion_info($course);&lt;br /&gt;
 if($completion-&amp;gt;is_enabled($cm) &amp;amp;&amp;amp; $forum-&amp;gt;completionposts) {&lt;br /&gt;
     $completion-&amp;gt;update_state($cm,COMPLETION_COMPLETE);&lt;br /&gt;
 }&lt;br /&gt;
=== Completion Checks in Cron Tasks ===&lt;br /&gt;
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. &lt;br /&gt;
==== Example ====&lt;br /&gt;
 require_once($CFG-&amp;gt;dirroot.&#039;/lib/completionlib.php&#039;);&lt;br /&gt;
== See Also ==&lt;br /&gt;
[[Conditional activities|Activity completion and availability]] - Original Specification&lt;br /&gt;
&lt;br /&gt;
[[Course completion]] - Original Specification&lt;br /&gt;
&lt;br /&gt;
[[Policy - Retroactive effects of completion settings]]&lt;br /&gt;
&lt;br /&gt;
[[Core APIs]]&lt;br /&gt;
=== User Docs ===&lt;br /&gt;
[https://docs.moodle.org/en/Category:Completion Completion Docs]&lt;br /&gt;
&lt;br /&gt;
[https://docs.moodle.org/en/Activity_completion Activity Completion]&lt;br /&gt;
&lt;br /&gt;
[https://docs.moodle.org/en/Course_completion Course Completion]&lt;br /&gt;
[[Category:Conditional activities]]&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Activity_completion_API&amp;diff=61640</id>
		<title>Activity completion API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Activity_completion_API&amp;diff=61640"/>
		<updated>2022-01-24T14:27:57Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Custom completion rules */ Added in documentation on Moodle 3.3 changes to the API&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle_2.0}}&lt;br /&gt;
{{Note|There are changes to the completion API introduced in &#039;&#039;&#039;Moodle 3.11&#039;&#039;&#039; to be incorporated to this page. Please refer to [[Student activity completion]] for details.}}&lt;br /&gt;
Modules do not need to be changed to support conditional availability, but they do need changing to support the completion system. &lt;br /&gt;
&lt;br /&gt;
If you make no changes to a module whatsoever, it can only support &#039;manual&#039; completion (where the user ticks a box).&lt;br /&gt;
== Feature support ==&lt;br /&gt;
To support the completion system, your module must include a &#039;&#039;modulename&#039;&#039;&amp;lt;tt&amp;gt;_supports&amp;lt;/tt&amp;gt; function in its &amp;lt;tt&amp;gt;lib.php&amp;lt;/tt&amp;gt;. Here is an example:&lt;br /&gt;
 /**&lt;br /&gt;
  * Indicates API features that the forum supports.&lt;br /&gt;
  *&lt;br /&gt;
  * @param string $feature&lt;br /&gt;
  * @return mixed True if yes (some features may use other values)&lt;br /&gt;
  */&lt;br /&gt;
 function forum_supports($feature) {&lt;br /&gt;
     switch($feature) {&lt;br /&gt;
         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;&lt;br /&gt;
         case FEATURE_COMPLETION_HAS_RULES: return true;&lt;br /&gt;
         default: return null;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
The relevant features for completion are:&lt;br /&gt;
* &#039;&#039;&#039;FEATURE_COMPLETION_TRACKS_VIEWS&#039;&#039;&#039; - the module can support completion &#039;on view&#039;, meaning that an activity becomes marked complete as soon as a user clicks on it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FEATURE_GRADE_HAS_GRADE&#039;&#039;&#039; - the module provides (or may provide, depending on settings) a grade for students. When a module supports grades, it can support completion &#039;on grade&#039;, meaning that an activity becomes marked complete as soon as a user is assigned a grade.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FEATURE_COMPLETION_HAS_RULES&#039;&#039;&#039; - the module has custom completion rules.&lt;br /&gt;
== Completion on view ==&lt;br /&gt;
Completion on view means that, if selected, an activity is marked complete as soon as the user views it. &#039;View&#039; is usually defined as seeing the module&#039;s main page; if you click on the activity, and there isn&#039;t an error, you have probably viewed it. However it is up to each module precisely how they define &#039;view&#039;.&lt;br /&gt;
=== How to implement ===&lt;br /&gt;
In your module&#039;s &#039;&#039;modulename&#039;&#039;&amp;lt;tt&amp;gt;_supports&amp;lt;/tt&amp;gt; function, return true for FEATURE_COMPLETION_TRACKS_VIEWS.&lt;br /&gt;
&lt;br /&gt;
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&#039;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.&lt;br /&gt;
 $completion = new completion_info($course);&lt;br /&gt;
 $completion-&amp;gt;set_module_viewed($cm);&lt;br /&gt;
=== Performance issues ===&lt;br /&gt;
Calling this method has no significant performance cost if &#039;on view&#039; completion is not enabled for the activity. If it is enabled, then the performance cost is kept low because the &#039;viewed&#039; state is cached; it doesn&#039;t add a database query to every request.&lt;br /&gt;
== Completion on grade ==&lt;br /&gt;
Completion on grade means that, if selected, an activity is marked complete as soon as the user receives a grade from that activity.&lt;br /&gt;
=== How to implement ===&lt;br /&gt;
In your module&#039;s _supports function, return true for FEATURE_GRADE_HAS_GRADE. No other action is necessary.&lt;br /&gt;
=== Performance issues ===&lt;br /&gt;
When &#039;on grade&#039; 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.&lt;br /&gt;
== Custom completion rules ==&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
It is a lot harder to implement custom completion rules than it is to use the system-provided &#039;view&#039; or &#039;grade&#039; conditions, but the instructions below should help make it clear.&lt;br /&gt;
=== Implementation overview ===&lt;br /&gt;
To implement custom completion rules, you need to:&lt;br /&gt;
# Return true for FEATURE_COMPLETION_HAS_RULES in your module&#039;s _supports function.&lt;br /&gt;
# Add database fields to your module&#039;s main table to store the custom completion settings.&lt;br /&gt;
# Add backup and restore code to back up these fields.&lt;br /&gt;
# Add information about the completion settings to the activities cm_info object&lt;br /&gt;
# Add controls to your module&#039;s settings form so that users can select the custom rules, altering these database settings.&lt;br /&gt;
# Add a function that checks the value of these rules (if set).&lt;br /&gt;
# Add function returns descriptions for the completion states&lt;br /&gt;
# Add code so that whenever the value affecting a rule might change, you inform the completion system.&lt;br /&gt;
=== Database fields for completion settings ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
Usually the best place to store this information is your module&#039;s main table because:&lt;br /&gt;
* 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.&lt;br /&gt;
* You already read this row with most requests, so there is no need for additional database queries which would reduce performance.&lt;br /&gt;
* The main table is used for most other module options so it is a logical place for this information.&lt;br /&gt;
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.&lt;br /&gt;
==== Example ====&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
The forum adds this field to store a completion option:&lt;br /&gt;
* &#039;&#039;&#039;completionposts&#039;&#039;&#039; - this may be 0 or an integer. If it&#039;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 &#039;completed&#039;.&lt;br /&gt;
=== Backup and restore for completion fields ===&lt;br /&gt;
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. &lt;br /&gt;
&lt;br /&gt;
Remember that your restore code should handle the case when these fields are not present, setting the fields to a suitable default value.&lt;br /&gt;
==== Example ====&lt;br /&gt;
The following code in &amp;lt;tt&amp;gt;backup_forum_stepslib.php&amp;lt;/tt&amp;gt; lists the fields to back up:&lt;br /&gt;
  $forum = new backup_nested_element(&#039;forum&#039;, array(&#039;id&#039;), array(&lt;br /&gt;
            &#039;type&#039;, &#039;name&#039;, &#039;intro&#039;, &#039;introformat&#039;,&lt;br /&gt;
            &#039;assessed&#039;, &#039;assesstimestart&#039;, &#039;assesstimefinish&#039;, &#039;scale&#039;,&lt;br /&gt;
            &#039;maxbytes&#039;, &#039;maxattachments&#039;, &#039;forcesubscribe&#039;, &#039;trackingtype&#039;,&lt;br /&gt;
            &#039;rsstype&#039;, &#039;rssarticles&#039;, &#039;timemodified&#039;, &#039;warnafter&#039;,&lt;br /&gt;
            &#039;blockafter&#039;, &#039;blockperiod&#039;, &#039;completiondiscussions&#039;, &#039;completionreplies&#039;,&lt;br /&gt;
            &#039;completionposts&#039;));&lt;br /&gt;
As you can see, I added the &#039;&#039;&#039;completionposts&#039;&#039;&#039; field (and the others that aren&#039;t covered in this example) to the list of fields.&lt;br /&gt;
&lt;br /&gt;
=== Add information about the completion settings to the activities cm_info object ===&lt;br /&gt;
You will need to add information about the custom rules into the activities cm_info object by either adding, or modifying the module_get_coursemodule_info callback&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Add a get_coursemodule_info function in case any forum type wants to add &#039;extra&#039; information&lt;br /&gt;
 * for the course (see resource).&lt;br /&gt;
 *&lt;br /&gt;
 * Given a course_module object, this function returns any &amp;quot;extra&amp;quot; information that may be needed&lt;br /&gt;
 * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.&lt;br /&gt;
 *&lt;br /&gt;
 * @param stdClass $coursemodule The coursemodule object (record).&lt;br /&gt;
 * @return cached_cm_info An object on information that the courses&lt;br /&gt;
 *                        will know about (most noticeably, an icon).&lt;br /&gt;
 */&lt;br /&gt;
function forum_get_coursemodule_info($coursemodule) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    $dbparams = [&#039;id&#039; =&amp;gt; $coursemodule-&amp;gt;instance];&lt;br /&gt;
    $fields = &#039;id, name, intro, introformat, completionposts, completiondiscussions, completionreplies, duedate, cutoffdate&#039;;&lt;br /&gt;
    if (!$forum = $DB-&amp;gt;get_record(&#039;forum&#039;, $dbparams, $fields)) {&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $result = new cached_cm_info();&lt;br /&gt;
    $result-&amp;gt;name = $forum-&amp;gt;name;&lt;br /&gt;
&lt;br /&gt;
    if ($coursemodule-&amp;gt;showdescription) {&lt;br /&gt;
        // Convert intro to html. Do not filter cached version, filters run at display time.&lt;br /&gt;
        $result-&amp;gt;content = format_module_intro(&#039;forum&#039;, $forum, $coursemodule-&amp;gt;id, false);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Populate the custom completion rules as key =&amp;gt; value pairs, but only if the completion mode is &#039;automatic&#039;.&lt;br /&gt;
    if ($coursemodule-&amp;gt;completion == COMPLETION_TRACKING_AUTOMATIC) {&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;customcompletionrules&#039;][&#039;completiondiscussions&#039;] = $forum-&amp;gt;completiondiscussions;&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;customcompletionrules&#039;][&#039;completionreplies&#039;] = $forum-&amp;gt;completionreplies;&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;customcompletionrules&#039;][&#039;completionposts&#039;] = $forum-&amp;gt;completionposts;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Populate some other values that can be used in calendar or on dashboard.&lt;br /&gt;
    if ($forum-&amp;gt;duedate) {&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;duedate&#039;] = $forum-&amp;gt;duedate;&lt;br /&gt;
    }&lt;br /&gt;
    if ($forum-&amp;gt;cutoffdate) {&lt;br /&gt;
        $result-&amp;gt;customdata[&#039;cutoffdate&#039;] = $forum-&amp;gt;cutoffdate;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return $result;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Form changes for completion settings ===&lt;br /&gt;
When you have custom completion conditions, you need to add controls to your module&#039;s settings form &amp;lt;tt&amp;gt;mod_form.php&amp;lt;/tt&amp;gt; so that users can select these conditions. You can add any necessary controls.&lt;br /&gt;
* Implement the &amp;lt;tt&amp;gt;add_completion_rules&amp;lt;/tt&amp;gt; function which adds the form controls for your new rules.&lt;br /&gt;
&lt;br /&gt;
* Implement the &amp;lt;tt&amp;gt;completion_rule_enabled&amp;lt;/tt&amp;gt; function which is called during form validation to check whether one of your module&#039;s completion rules has been selected.&lt;br /&gt;
&lt;br /&gt;
* 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.&lt;br /&gt;
==== Example ====&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
First, the function that adds these controls:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Add elements for setting the custom completion rules.&lt;br /&gt;
 *  &lt;br /&gt;
 * @category completion&lt;br /&gt;
 * @return array List of added element names, or names of wrapping group elements.&lt;br /&gt;
 */&lt;br /&gt;
public function add_completion_rules() {&lt;br /&gt;
&lt;br /&gt;
    $mform = $this-&amp;gt;_form;&lt;br /&gt;
&lt;br /&gt;
    $group = [&lt;br /&gt;
        $mform-&amp;gt;createElement(&#039;checkbox&#039;, &#039;completionpostsenabled&#039;, &#039; &#039;, get_string(&#039;completionposts&#039;, &#039;forum&#039;)),&lt;br /&gt;
        $mform-&amp;gt;createElement(&#039;text&#039;, &#039;completionposts&#039;, &#039; &#039;, [&#039;size&#039; =&amp;gt; 3]),&lt;br /&gt;
    ];&lt;br /&gt;
    $mform-&amp;gt;setType(&#039;completionposts&#039;, PARAM_INT);&lt;br /&gt;
    $mform-&amp;gt;addGroup($group, &#039;completionpostsgroup&#039;, get_string(&#039;completionpostsgroup&#039;,&#039;forum&#039;), [&#039; &#039;], false);&lt;br /&gt;
    $mform-&amp;gt;addHelpButton(&#039;completionpostsgroup&#039;, &#039;completionposts&#039;, &#039;forum&#039;);&lt;br /&gt;
    $mform-&amp;gt;disabledIf(&#039;completionposts&#039;, &#039;completionpostsenabled&#039;, &#039;notchecked&#039;);&lt;br /&gt;
&lt;br /&gt;
    return [&#039;completionpostsgroup&#039;];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* The function creates a checkbox and a text input field, which is set to accept only numbers.&lt;br /&gt;
* These are grouped together so they appear on the same line, and we add a help button.&lt;br /&gt;
* The text input field is disabled if the checkbox isn&#039;t ticked.&lt;br /&gt;
* 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.)&lt;br /&gt;
** This is used so that your controls become disabled if automatic completion is not selected.&lt;br /&gt;
Next, a function for checking whether the user selected this option:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Called during validation to see whether some module-specific completion rules are selected.&lt;br /&gt;
 *&lt;br /&gt;
 * @param array $data Input data not yet validated.&lt;br /&gt;
 * @return bool True if one or more rules is enabled, false if none are.&lt;br /&gt;
 */&lt;br /&gt;
public function completion_rule_enabled($data) {&lt;br /&gt;
    return (!empty($data[&#039;completionpostsenabled&#039;]) &amp;amp;&amp;amp; $data[&#039;completionposts&#039;] != 0);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
* The custom completion rule is enabled if the &#039;enabled&#039; checkbox is ticked and the text field value is something other than zero.&lt;br /&gt;
** This is used to give an error if the user selects automatic completion, but fails to select any conditions.&lt;br /&gt;
That&#039;s all the &#039;required&#039; 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).&lt;br /&gt;
 function get_data() {&lt;br /&gt;
     $data = parent::get_data();&lt;br /&gt;
     if (!$data) {&lt;br /&gt;
         return $data;&lt;br /&gt;
     }&lt;br /&gt;
     if (!empty($data-&amp;gt;completionunlocked)) {&lt;br /&gt;
         // Turn off completion settings if the checkboxes aren&#039;t ticked&lt;br /&gt;
         $autocompletion = !empty($data-&amp;gt;completion) &amp;amp;&amp;amp; $data-&amp;gt;completion==COMPLETION_TRACKING_AUTOMATIC;&lt;br /&gt;
         if (empty($data-&amp;gt;completionpostsenabled) || !$autocompletion) {&lt;br /&gt;
            $data-&amp;gt;completionposts = 0;&lt;br /&gt;
         }&lt;br /&gt;
     }&lt;br /&gt;
     return $data;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
You may have noticed the &#039;completionunlocked&#039; check. When some users have already completed the activity, the completion settings are &#039;locked&#039;; they are disabled and cannot be edited, so there will be no value set for those fields in the &amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;$data&amp;lt;/syntaxhighlight&amp;gt; object. Normally this will automatically work but when dealing with checkboxes you need to include a check for the &#039;completionunlocked&#039; value before doing anything that would cause one of those fields to be changed in the database.&lt;br /&gt;
Finally, forum already had a &amp;lt;tt&amp;gt;data_preprocessing&amp;lt;/tt&amp;gt; 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:&lt;br /&gt;
 function data_preprocessing(&amp;amp;$default_values){&lt;br /&gt;
     // [Existing code, not shown]&lt;br /&gt;
 &lt;br /&gt;
     // Set up the completion checkboxes which aren&#039;t part of standard data.&lt;br /&gt;
     // We also make the default value (if you turn on the checkbox) for those&lt;br /&gt;
     // numbers to be 1, this will not apply unless checkbox is ticked.&lt;br /&gt;
     $default_values[&#039;completionpostsenabled&#039;]=&lt;br /&gt;
         !empty($default_values[&#039;completionposts&#039;]) ? 1 : 0;&lt;br /&gt;
     if(empty($default_values[&#039;completionposts&#039;])) {&lt;br /&gt;
         $default_values[&#039;completionposts&#039;]=1;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
Phew! That&#039;s the form done.&lt;br /&gt;
=== Completion state function ===&lt;br /&gt;
When you create completion conditions, you need to write a function &#039;&#039;module&#039;&#039;&amp;lt;tt&amp;gt;_get_completion_state&amp;lt;/tt&amp;gt; that checks the value of those conditions for a particular user. &lt;br /&gt;
&lt;br /&gt;
The function receives as parameters &amp;lt;tt&amp;gt;$course&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;$cm&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;$userid&amp;lt;/tt&amp;gt; - all self-explanatory, I hope - and &amp;lt;tt&amp;gt;$type&amp;lt;/tt&amp;gt;. This has two values: &lt;br /&gt;
* &#039;&#039;&#039;COMPLETION_AND&#039;&#039;&#039; - if multiple conditions are selected, the user must meet all of them.&lt;br /&gt;
* &#039;&#039;&#039;COMPLETION_OR&#039;&#039;&#039; (not currently used) - if multiple conditions are selected, any one of them is good enough to complete the activity.&lt;br /&gt;
Your function should return:&lt;br /&gt;
* &#039;&#039;&#039;true&#039;&#039;&#039; if your custom completion options are enabled and the user meets the conditions.&lt;br /&gt;
* &#039;&#039;&#039;false&#039;&#039;&#039; if your custom completion options are enabled but the user does not yet meet the conditions.&lt;br /&gt;
* &amp;lt;tt&amp;gt;$type&amp;lt;/tt&amp;gt; (not false!) if none of your custom completion options are not enabled.&lt;br /&gt;
==== Example ====&lt;br /&gt;
Here&#039;s the function for forum (simplified to include only the one completion option):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
 /**&lt;br /&gt;
  * Obtains the automatic completion state for this forum based on any conditions&lt;br /&gt;
  * in forum settings.&lt;br /&gt;
  *&lt;br /&gt;
  * @param object $course Course&lt;br /&gt;
  * @param object $cm Course-module&lt;br /&gt;
  * @param int $userid User ID&lt;br /&gt;
  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)&lt;br /&gt;
  * @return bool True if completed, false if not, $type if conditions not set.&lt;br /&gt;
  */&lt;br /&gt;
 function forum_get_completion_state($course,$cm,$userid,$type) {&lt;br /&gt;
     global $CFG,$DB;&lt;br /&gt;
 &lt;br /&gt;
     // Get forum details&lt;br /&gt;
     $forum = $DB-&amp;gt;get_record(&#039;forum&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance), &#039;*&#039;, MUST_EXIST);&lt;br /&gt;
 &lt;br /&gt;
     // If completion option is enabled, evaluate it and return true/false &lt;br /&gt;
     if($forum-&amp;gt;completionposts) {&lt;br /&gt;
         return $forum-&amp;gt;completionposts &amp;lt;= $DB-&amp;gt;get_field_sql(&amp;quot;&lt;br /&gt;
 SELECT &lt;br /&gt;
     COUNT(1) &lt;br /&gt;
 FROM &lt;br /&gt;
     {forum_posts} fp &lt;br /&gt;
     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id&lt;br /&gt;
 WHERE&lt;br /&gt;
     fp.userid=:userid AND fd.forum=:forumid&amp;quot;,&lt;br /&gt;
             array(&#039;userid&#039;=&amp;gt;$userid,&#039;forumid&#039;=&amp;gt;$forum-&amp;gt;id));&lt;br /&gt;
     } else {&lt;br /&gt;
         // Completion option is not enabled so just return $type&lt;br /&gt;
         return $type;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Add function returns descriptions for the completion states ===&lt;br /&gt;
When you create completion conditions, you need to write a function &#039;&#039;module&#039;&#039;&amp;lt;tt&amp;gt;_get_completion_active_rule_descriptions&amp;lt;/tt&amp;gt; that gives a human readable description of the completion state.&lt;br /&gt;
&lt;br /&gt;
The input for the method is the cm_info object for that activity.&lt;br /&gt;
&lt;br /&gt;
You need to return an array of strings for each completion rule that is active.&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Callback which returns human-readable strings describing the active completion custom rules for the module instance.&lt;br /&gt;
 *&lt;br /&gt;
 * @param cm_info|stdClass $cm object with fields -&amp;gt;completion and -&amp;gt;customdata[&#039;customcompletionrules&#039;]&lt;br /&gt;
 * @return array $descriptions the array of descriptions for the custom rules.&lt;br /&gt;
 */&lt;br /&gt;
function mod_forum_get_completion_active_rule_descriptions($cm) {&lt;br /&gt;
    // Values will be present in cm_info, and we assume these are up to date.&lt;br /&gt;
    if (empty($cm-&amp;gt;customdata[&#039;customcompletionrules&#039;])&lt;br /&gt;
        || $cm-&amp;gt;completion != COMPLETION_TRACKING_AUTOMATIC) {&lt;br /&gt;
        return [];&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $descriptions = [];&lt;br /&gt;
    foreach ($cm-&amp;gt;customdata[&#039;customcompletionrules&#039;] as $key =&amp;gt; $val) {&lt;br /&gt;
        switch ($key) {&lt;br /&gt;
            case &#039;completiondiscussions&#039;:&lt;br /&gt;
                if (!empty($val)) {&lt;br /&gt;
                    $descriptions[] = get_string(&#039;completiondiscussionsdesc&#039;, &#039;forum&#039;, $val);&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            case &#039;completionreplies&#039;:&lt;br /&gt;
                if (!empty($val)) {&lt;br /&gt;
                    $descriptions[] = get_string(&#039;completionrepliesdesc&#039;, &#039;forum&#039;, $val);&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            case &#039;completionposts&#039;:&lt;br /&gt;
                if (!empty($val)) {&lt;br /&gt;
                    $descriptions[] = get_string(&#039;completionpostsdesc&#039;, &#039;forum&#039;, $val);&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            default:&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    return $descriptions;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Notifying the completion system ===&lt;br /&gt;
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. &lt;br /&gt;
* To ensure performance is not compromised, you should notify the system only when the completion state might actually have changed. Don&#039;t notify the system unless your custom completion rule is actually enabled.&lt;br /&gt;
* You need to pass in the &#039;possible result&#039; of the change. This is used to significantly improve performance. There are three values:&lt;br /&gt;
**&#039;&#039;&#039;COMPLETION_COMPLETE&#039;&#039;&#039; - this change will either have no effect on the user&#039;s completion state, or it will make it complete. The change cannot make a user&#039;s state &#039;&#039;in&#039;&#039;complete if it was complete previously. In the forum example, when you add a post, there is no way this can make the user&#039;s state incomplete, so this possible result applies.&lt;br /&gt;
** &#039;&#039;&#039;COMPLETION_INCOMPLETE&#039;&#039;&#039; - this change will either have no effect on the user&#039;s completion state, or it will make it incomplete. The change cannot make a user&#039;s state complete if it was incomplete previously. Deleting a forum post would fall into this category.&lt;br /&gt;
** &#039;&#039;&#039;COMPLETION_UNKNOWN&#039;&#039;&#039; - 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.&lt;br /&gt;
* If the user whose completion state would be updated is not the current user, then the optional &amp;lt;tt&amp;gt;$userid&amp;lt;/tt&amp;gt; parameter must be included. For example, if a teacher deletes a student&#039;s forum post, then it is the student&#039;s completion state which may need updating, not the teacher&#039;s.&lt;br /&gt;
==== Example ====&lt;br /&gt;
Here&#039;s the code that runs when somebody makes a new forum post:&lt;br /&gt;
 // Update completion state&lt;br /&gt;
 $completion=new completion_info($course);&lt;br /&gt;
 if($completion-&amp;gt;is_enabled($cm) &amp;amp;&amp;amp; $forum-&amp;gt;completionposts) {&lt;br /&gt;
     $completion-&amp;gt;update_state($cm,COMPLETION_COMPLETE);&lt;br /&gt;
 }&lt;br /&gt;
=== Completion Checks in Cron Tasks ===&lt;br /&gt;
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. &lt;br /&gt;
==== Example ====&lt;br /&gt;
 require_once($CFG-&amp;gt;dirroot.&#039;/lib/completionlib.php&#039;);&lt;br /&gt;
== See Also ==&lt;br /&gt;
[[Conditional activities|Activity completion and availability]] - Original Specification&lt;br /&gt;
&lt;br /&gt;
[[Course completion]] - Original Specification&lt;br /&gt;
&lt;br /&gt;
[[Policy - Retroactive effects of completion settings]]&lt;br /&gt;
&lt;br /&gt;
[[Core APIs]]&lt;br /&gt;
=== User Docs ===&lt;br /&gt;
[https://docs.moodle.org/en/Category:Completion Completion Docs]&lt;br /&gt;
&lt;br /&gt;
[https://docs.moodle.org/en/Activity_completion Activity Completion]&lt;br /&gt;
&lt;br /&gt;
[https://docs.moodle.org/en/Course_completion Course Completion]&lt;br /&gt;
[[Category:Conditional activities]]&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing/Browsers&amp;diff=58549</id>
		<title>Acceptance testing/Browsers</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing/Browsers&amp;diff=58549"/>
		<updated>2021-03-19T13:17:03Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Drivers */  Updated location of edge web driver&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This page complements [[Acceptance testing]] providing info about how to run the acceptance tests suite in different browsers.&lt;br /&gt;
&lt;br /&gt;
== Drivers ==&lt;br /&gt;
&lt;br /&gt;
There are Selenium drivers (http://docs.seleniumhq.org/projects/webdriver/) to run acceptance tests in different browsers:&lt;br /&gt;
&lt;br /&gt;
* Firefox - https://code.google.com/p/selenium/wiki/FirefoxDriver&lt;br /&gt;
* Chrome - https://code.google.com/p/selenium/wiki/ChromeDriver&lt;br /&gt;
* Safari - https://code.google.com/p/selenium/wiki/SafariDriver&lt;br /&gt;
* Internet Explorer - https://code.google.com/p/selenium/wiki/InternetExplorerDriver&lt;br /&gt;
* Microsoft Edge - https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/&lt;br /&gt;
* PhantomJS (Webkit) - http://phantomjs.org/&lt;br /&gt;
* IPhone - https://code.google.com/p/selenium/wiki/IPhoneDriver&lt;br /&gt;
&lt;br /&gt;
Each driver should be downloaded and Selenium .jar should be started specifying the path to the driver; depending on the driver there could be other requirements. &lt;br /&gt;
&lt;br /&gt;
=== PhantomJS ===&lt;br /&gt;
&lt;br /&gt;
PhantomJS is different as it is a headless browser as it is quite faster than other drivers, it doesn&#039;t need a GUI to run and can execute JS, it doesn&#039;t even need to be used through Selenium (you can do it though, but it&#039;s not officially supported) and you can do it&lt;br /&gt;
&lt;br /&gt;
* Download PhantomJS: http://phantomjs.org/download.html&lt;br /&gt;
* /path/to/your/phantomjs/bin/phantomjs --webdriver=4444&lt;br /&gt;
&lt;br /&gt;
Note that 4444 is the default port used by Selenium so you must specify another one if you want to run them together and specify the port in $CFG-&amp;gt;behat_config.&lt;br /&gt;
&lt;br /&gt;
=== Examples ===&lt;br /&gt;
&lt;br /&gt;
  # Selenium in Linux (firefox by default + chrome)&lt;br /&gt;
  java -jar /opt/selenium-server-standalone.jar -Dwebdriver.chrome.driver=/opt/chromedriver&lt;br /&gt;
&lt;br /&gt;
  # Selenium in OSx (firefox &amp;amp; safari by default + chrome)&lt;br /&gt;
  java -jar /Users/moodle/Downloads/selenium-server-standalone.jar -Dwebdriver.chrome.driver=/Users/moodle/Downloads/chromedriver&lt;br /&gt;
&lt;br /&gt;
  # Selenium in Windows (started using git bash) (firefox by default + chrome + internet explorer)&lt;br /&gt;
  java -jar /c/seleniumdrivers/selenium-server-standalone.jar -Dwebdriver.chrome.driver=/c/seleniumdrivers/chromedriver.exe -Dwebdriver.ie.driver=/c/seleniumdrivers/IEDriverServer.exe&lt;br /&gt;
&lt;br /&gt;
  # PhantomJS&lt;br /&gt;
  /path/to/your/phantomjs/bin/phantomjs --webdriver=4444&lt;br /&gt;
&lt;br /&gt;
== Compatibility ==&lt;br /&gt;
Not all the drivers can execute all of Moodle&#039;s step definitions; we tagged the step definitions that are using features not supported by all browsers and also limitations that some browsers have; refer to the following table to know which browsers can run which tags:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|&lt;br /&gt;
|File uploads (@_file_upload)&lt;br /&gt;
|Browser dialogs (@_alert)&lt;br /&gt;
|Switch window (@_switch_window)&lt;br /&gt;
|Switch frame (@_switch_iframe)&lt;br /&gt;
|Bug in phantomjs (@_bug_phantomjs)&lt;br /&gt;
|-&lt;br /&gt;
|Firefox&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Chrome&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Internet Explorer&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;No&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|Safari&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;No&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;No&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|PhantomJS&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;No&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;No&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:#0f0&amp;quot;&amp;gt;Yes&amp;lt;/span&amp;gt;&lt;br /&gt;
|&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;No&amp;lt;/span&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Working combinations of OS+Browser+selenium ==&lt;br /&gt;
As OS, Browsers and Selenium keeps updating, some combination of OS+Browser+Selenium will not work on specific moodle version.&lt;br /&gt;
&lt;br /&gt;
We try to support the latest version of these combinations but they are not always BC and hence may not work with older releases. Please refer to [[Acceptance testing/Browsers/Working combinations of OS+Browser+selenium]] to ensure you have correct combination of them to run acceptance test.&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=56447</id>
		<title>Moodle App Plugins Development Guide</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=56447"/>
		<updated>2019-09-13T09:12:10Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Before 3.5 */ Fix link&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
==Before 3.5==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle_Mobile_2_(Ionic_1)_Remote_add-ons|Remote add-ons]] for details.&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5 the app switched to a new way to support plugins that was much easier for developers.&lt;br /&gt;
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).&lt;br /&gt;
* The use of JavaScript is optional (but some type of advanced plugins may require it)&lt;br /&gt;
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).&lt;br /&gt;
&lt;br /&gt;
This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)&lt;br /&gt;
&lt;br /&gt;
This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.&lt;br /&gt;
&lt;br /&gt;
==How it works==&lt;br /&gt;
&lt;br /&gt;
The overall idea is to allow Moodle plugins to extend different areas in the app with &#039;&#039;just PHP server side&#039;&#039; code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.&lt;br /&gt;
&lt;br /&gt;
Developers will have to:&lt;br /&gt;
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.&lt;br /&gt;
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates. &lt;br /&gt;
&lt;br /&gt;
Let’s clarify some points:&lt;br /&gt;
&lt;br /&gt;
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.&lt;br /&gt;
* Those functions will be exported via the Web Service function tool_mobile_get_content&lt;br /&gt;
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).&lt;br /&gt;
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..&lt;br /&gt;
&lt;br /&gt;
==Types of plugins==&lt;br /&gt;
&lt;br /&gt;
We could classify all the plugins in 3 different types:&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_when_requested.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you&#039;re developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_on_login.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
==Step by step example==&lt;br /&gt;
&lt;br /&gt;
In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.&lt;br /&gt;
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.&lt;br /&gt;
&lt;br /&gt;
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)&lt;br /&gt;
&lt;br /&gt;
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.&lt;br /&gt;
&lt;br /&gt;
===Step 1. Update the db/mobile.php file===&lt;br /&gt;
In this case, we are updating an existing file but for new plugins, you should create this new file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$addons = [&lt;br /&gt;
    &#039;mod_certificate&#039; =&amp;gt; [ // Plugin identifier&lt;br /&gt;
        &#039;handlers&#039; =&amp;gt; [ // Different places where the plugin will display content.&lt;br /&gt;
            &#039;coursecertificate&#039; =&amp;gt; [ // Handler unique name (alphanumeric).&lt;br /&gt;
                &#039;displaydata&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;icon&#039; =&amp;gt; $CFG-&amp;gt;wwwroot . &#039;/mod/certificate/pix/icon.gif&#039;,&lt;br /&gt;
                    &#039;class&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
                ],&lt;br /&gt;
       &lt;br /&gt;
                &#039;delegate&#039; =&amp;gt; &#039;CoreCourseModuleDelegate&#039;, // Delegate (where to display the link to the plugin)&lt;br /&gt;
                &#039;method&#039; =&amp;gt; &#039;mobile_course_view&#039;, // Main function in \mod_certificate\output\mobile&lt;br /&gt;
                &#039;offlinefunctions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;mobile_course_view&#039; =&amp;gt; [],&lt;br /&gt;
                    &#039;mobile_issues_view&#039; =&amp;gt; [],&lt;br /&gt;
                ], // Function that needs to be downloaded for offline.&lt;br /&gt;
            ],&lt;br /&gt;
        ],&lt;br /&gt;
        &#039;lang&#039; =&amp;gt; [ // Language strings that are used in all the handlers.&lt;br /&gt;
            [&#039;pluginname&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;summaryofattempts&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;getcertificate&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;requiredtimenotmet&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;viewcertificateviews&#039;, &#039;certificate&#039;],&lt;br /&gt;
        ],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Plugin identifier:&lt;br /&gt;
: A unique name for the plugin, it can be anything (there’s no need to match the module name).&lt;br /&gt;
 &lt;br /&gt;
;Handlers  (Different places where the plugin will display content):&lt;br /&gt;
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).&lt;br /&gt;
&lt;br /&gt;
; Display data:&lt;br /&gt;
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.&lt;br /&gt;
	&lt;br /&gt;
; Delegate&lt;br /&gt;
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.&lt;br /&gt;
&lt;br /&gt;
; Method:&lt;br /&gt;
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.	&lt;br /&gt;
&lt;br /&gt;
; Offlinefunctions&lt;br /&gt;
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file. &lt;br /&gt;
: In our example, downloading for offline access will mean that we&#039;ll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we&#039;ll be able to display the certificate information even if the user is offline.&lt;br /&gt;
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).&lt;br /&gt;
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)&lt;br /&gt;
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the &#039;&#039;files&#039;&#039; array).&lt;br /&gt;
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module&#039;s view function by using a &#039;page&#039; parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].&lt;br /&gt;
&lt;br /&gt;
;Lang:&lt;br /&gt;
: &amp;lt;nowiki&amp;gt;The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here (e.g. [&#039;cancel&#039;, &#039;moodle&#039;]). If you do this, be warned that in the app you will then need to refer to that string as {{ &#039;plugin.myplugin.cancel&#039; | translate }} (not {{ &#039;plugin.moodle.cancel&#039; | translate }})&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
: Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful.&lt;br /&gt;
&lt;br /&gt;
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.&lt;br /&gt;
&lt;br /&gt;
===Step 2. Creating the main function===&lt;br /&gt;
&lt;br /&gt;
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.&lt;br /&gt;
&lt;br /&gt;
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.&lt;br /&gt;
&lt;br /&gt;
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
namespace mod_certificate\output;&lt;br /&gt;
&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
&lt;br /&gt;
use context_module;&lt;br /&gt;
use mod_certificate_external;&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Mobile output class for certificate&lt;br /&gt;
 *&lt;br /&gt;
 * @package    mod_certificate&lt;br /&gt;
 * @copyright  2018 Juan Leyva&lt;br /&gt;
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later&lt;br /&gt;
 */&lt;br /&gt;
class mobile {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate course view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_course_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Set timemodified for each certificate.&lt;br /&gt;
        foreach ($issues as $issue) {&lt;br /&gt;
            if (empty($issue-&amp;gt;timemodified)) {&lt;br /&gt;
                    $issue-&amp;gt;timemodified = $issue-&amp;gt;timecreated;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $showget = true;&lt;br /&gt;
        if ($certificate-&amp;gt;requiredtime &amp;amp;&amp;amp; !has_capability(&#039;mod/certificate:manage&#039;, $context)) {&lt;br /&gt;
            if (certificate_get_course_time($certificate-&amp;gt;course) &amp;lt; ($certificate-&amp;gt;requiredtime * 60)) {&lt;br /&gt;
                    $showget = false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $certificate-&amp;gt;name = format_string($certificate-&amp;gt;name);&lt;br /&gt;
        list($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat) =&lt;br /&gt;
                        external_format_text($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat, $context-&amp;gt;id,&#039;mod_certificate&#039;, &#039;intro&#039;);&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;certificate&#039; =&amp;gt; $certificate,&lt;br /&gt;
            &#039;showget&#039; =&amp;gt; $showget &amp;amp;&amp;amp; count($issues) &amp;gt; 0,&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues,&lt;br /&gt;
            &#039;issue&#039; =&amp;gt; $issues[0],&lt;br /&gt;
            &#039;numissues&#039; =&amp;gt; count($issues),&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;files&#039; =&amp;gt; $issues,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let’s go through the function code to analyse the different parts.&lt;br /&gt;
&lt;br /&gt;
;Function declaration: &lt;br /&gt;
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument “$args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)&lt;br /&gt;
&lt;br /&gt;
; Function implementation:&lt;br /&gt;
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.&lt;br /&gt;
&lt;br /&gt;
Finally, we return:&lt;br /&gt;
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.&lt;br /&gt;
* JavaScript: Empty, because we don’t need any in this case&lt;br /&gt;
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.&lt;br /&gt;
* Files: A list of files that the app should be able to download (for offline usage mostly)&lt;br /&gt;
&lt;br /&gt;
===Step 3. Creating the template for the main function===&lt;br /&gt;
&lt;br /&gt;
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.&lt;br /&gt;
&lt;br /&gt;
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.&lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/ &lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with &#039;&#039;core-&#039;&#039; are custom components of the Mobile app.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_page.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;core-course-module-description description=&amp;quot;&amp;lt;% certificate.intro %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-course-module-description&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;ion-list-header&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-list-header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
                    {{ &#039;plugin.mod_certificate.viewcertificateviews&#039; | translate: {$a: &amp;lt;% numissues %&amp;gt;} }}&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;ion-icon name=&amp;quot;cloud-download&amp;quot; item-start&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
                {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%^showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;p&amp;gt;{{ &#039;plugin.mod_certificate.requiredtimenotmet&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;!-- Call log WS when the template is loaded. --&amp;gt;&lt;br /&gt;
        &amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Then we display the module description using &amp;lt;code&amp;gt;&amp;lt;core-course-module-description&amp;lt;/code&amp;gt; that is a component used to include the course module description.&lt;br /&gt;
&lt;br /&gt;
For displaying the certificate information we create a list of elements, adding a header on top.&lt;br /&gt;
The following line &amp;lt;code&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/code&amp;gt; indicates that the Mobile app will translate the &#039;&#039;summaryofattempts&#039;&#039; string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format: &lt;br /&gt;
&lt;br /&gt;
“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php). &lt;br /&gt;
&lt;br /&gt;
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) &amp;lt;code&amp;gt;core-site-plugins-new-content&amp;lt;/code&amp;gt; indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).&lt;br /&gt;
&lt;br /&gt;
Just after this button we display another one but this time for downloading an issued certificate. The &amp;lt;code&amp;gt;core-course-download-module-main-file&amp;lt;/code&amp;gt; directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.&lt;br /&gt;
&lt;br /&gt;
Finally, just before the ion-list is closed, we use the &amp;lt;code&amp;gt;core-site-plugins-call-ws-on-load&amp;lt;/code&amp;gt; directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the &#039;&#039;mod_certificate_view_certificate&#039;&#039; that will log that the user viewed this page.&lt;br /&gt;
&lt;br /&gt;
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.&lt;br /&gt;
&lt;br /&gt;
===Step 4. Adding an additional page===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Partial file contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate issues view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_issues_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = [&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_issues&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.&lt;br /&gt;
&lt;br /&gt;
The code of the mustache template is also very simple:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_issues.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &amp;lt;%timecreated%&amp;gt; | coreToLocaleString }}&amp;lt;/p&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;&amp;lt;%grade%&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Here we are creating an ionic list that will display a new item in the list per each issued certificated.&lt;br /&gt;
&lt;br /&gt;
For the issued certificated we’ll display the time when it was created (using the app filter &#039;&#039;coreToLocaleString&#039;&#039;). We are also displaying the grade displayed in the certificate (if any).&lt;br /&gt;
&lt;br /&gt;
===Step 5. Plugin webservices, if included===&lt;br /&gt;
&lt;br /&gt;
If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.&lt;br /&gt;
&lt;br /&gt;
The following line &amp;lt;code&amp;gt;&#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&amp;lt;/code&amp;gt; should be included in each webservice definition.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/db/services.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$functions = [&lt;br /&gt;
&lt;br /&gt;
    &#039;mod_certificate_get_certificates_by_courses&#039; =&amp;gt; [&lt;br /&gt;
        &#039;classname&#039;     =&amp;gt; &#039;mod_certificate_external&#039;,&lt;br /&gt;
        &#039;methodname&#039;    =&amp;gt; &#039;get_certificates_by_courses&#039;,&lt;br /&gt;
        &#039;description&#039;   =&amp;gt; &#039;Returns a list of certificate instances...&#039;,&lt;br /&gt;
        &#039;type&#039;          =&amp;gt; &#039;read&#039;,&lt;br /&gt;
        &#039;capabilities&#039;  =&amp;gt; &#039;mod/certificate:view&#039;,&lt;br /&gt;
        &#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
	&lt;br /&gt;
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.&lt;br /&gt;
&lt;br /&gt;
==Getting started==&lt;br /&gt;
&lt;br /&gt;
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!&lt;br /&gt;
&lt;br /&gt;
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app.&lt;br /&gt;
&lt;br /&gt;
Please test that your site works correctly in the web version before starting any development.&lt;br /&gt;
&lt;br /&gt;
===Moodle version requirements===&lt;br /&gt;
&lt;br /&gt;
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin]. &lt;br /&gt;
&lt;br /&gt;
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).&lt;br /&gt;
&lt;br /&gt;
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.&lt;br /&gt;
&lt;br /&gt;
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.&lt;br /&gt;
&lt;br /&gt;
===Development workflow===&lt;br /&gt;
&lt;br /&gt;
First of all, we recommend creating a simple &#039;&#039;mobile.php&#039;&#039; for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Check that you can correctly  see the new menu option you included.&lt;br /&gt;
&lt;br /&gt;
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.&lt;br /&gt;
&lt;br /&gt;
It is important to remember that:&lt;br /&gt;
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).&lt;br /&gt;
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).&lt;br /&gt;
&lt;br /&gt;
===Testing and debugging===&lt;br /&gt;
&lt;br /&gt;
To learn how to debug with the web version of the app, please read the following documents:&lt;br /&gt;
* [[Moodle Mobile debugging WS requests]] AND&lt;br /&gt;
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)&lt;br /&gt;
&lt;br /&gt;
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.&lt;br /&gt;
&lt;br /&gt;
Within the app, make sure to turn on the option: &#039;&#039;&#039;App settings&#039;&#039;&#039; / &#039;&#039;&#039;General&#039;&#039;&#039; / &#039;&#039;&#039;Display debug messages&#039;&#039;&#039;. This means popup errors from the app will show more information.&lt;br /&gt;
&lt;br /&gt;
==Mobile.php supported options==&lt;br /&gt;
&lt;br /&gt;
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:&lt;br /&gt;
&lt;br /&gt;
===Common options===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;delegate&#039;&#039;&#039; (mandatory): Name of the delegate to register the handler in.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (mandatory): The function to call to retrieve the main page content.&lt;br /&gt;
* &#039;&#039;&#039;init&#039;&#039;&#039; (optional): A function to call to retrieve the initialization JS and the &amp;quot;restrict&amp;quot; to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.&lt;br /&gt;
* &#039;&#039;&#039;restricttocurrentuser&#039;&#039;&#039; (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;restricttoenrolledcourses&#039;&#039;&#039; (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;styles&#039;&#039;&#039; (optional): An array with two properties: &#039;&#039;url&#039;&#039; and &#039;&#039;version&#039;&#039;. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It&#039;s recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.&lt;br /&gt;
* &#039;&#039;&#039;moodlecomponent&#039;&#039;&#039; (optional): If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be &#039;&#039;local_whatever&#039;&#039;, but in &amp;quot;moodlecomponent&amp;quot; you can specify that this handler will implement &#039;&#039;format_whatever&#039;&#039; or &#039;&#039;mod_whatever&#039;&#039;. This property was introduced in the version 3.6.1 of the app.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseOptionsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreMainMenuDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the &amp;quot;More&amp;quot; tab, they cannot be displayed as tabs in the bottom bar.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseModuleDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): icon, class.&lt;br /&gt;
* &#039;&#039;&#039;offlinefunctions&#039;&#039;&#039;: (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + &#039;id&#039; (e.g. certificateid).&lt;br /&gt;
* &#039;&#039;&#039;downloadbutton&#039;&#039;&#039;: (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.&lt;br /&gt;
* &#039;&#039;&#039;isresource&#039;&#039;&#039;: (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the &amp;quot;contents&amp;quot; field, then it should be true.&lt;br /&gt;
* &#039;&#039;&#039;updatesnames&#039;&#039;&#039;: (optional) Only used if there is any offlinefunction. A Regular Expression to check if there&#039;s any update in the module. It will be compared to the result of &#039;&#039;core_course_check_updates&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;displayopeninbrowser&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Open in browser&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;&lt;br /&gt;
* &#039;&#039;&#039;displaydescription&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Description&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;&lt;br /&gt;
* &#039;&#039;&#039;displayrefresh&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Refresh&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;&lt;br /&gt;
* &#039;&#039;&#039;displayprefetch&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;&lt;br /&gt;
* &#039;&#039;&#039;displaysize&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseFormatDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;canviewallsections&#039;&#039;&#039;: (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displayenabledownload&#039;&#039;&#039;: (optional) Whether the option to enable section/module download should be displayed. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displaysectionselector&#039;&#039;&#039;: (optional) Whether the default section selector should be displayed. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreUserDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039;: The type of the addon. Values accepted: &#039;newpage&#039; (default) or  &#039;communication&#039;. &lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreSettingsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for AddonMessageOutputDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreBlockDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (optional): title, class. If &#039;&#039;title&#039;&#039; is not supplied, it will default to &amp;quot;plugins.block_blockname.pluginname&amp;quot;, where &#039;&#039;blockname&#039;&#039; is the name of the block. If &#039;&#039;class&#039;&#039; is not supplied, it will default to &amp;quot;block_blockname&amp;quot;, where &#039;&#039;blockname&#039;&#039; is the name of the block.&lt;br /&gt;
&lt;br /&gt;
==Delegates==&lt;br /&gt;
&lt;br /&gt;
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
====CoreMainMenuDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). &lt;br /&gt;
&lt;br /&gt;
====CoreCourseOptionsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).&lt;br /&gt;
&lt;br /&gt;
====CoreCourseModuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting activity modules or resources.&lt;br /&gt;
&lt;br /&gt;
====CoreUserDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add additional options in the user profile page in the app.&lt;br /&gt;
&lt;br /&gt;
====CoreCourseFormatDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting course formats.  When you open a course from the course list in the mobile app, it will check if there is a CoreCourseFormatDelegate handler for the format that site uses.  If so, it will display the course using that handler.  Otherwise, it will use the default app course format.  More information is available on [[Creating mobile course formats]].&lt;br /&gt;
&lt;br /&gt;
====CoreSettingsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to add a new option in the settings page.&lt;br /&gt;
&lt;br /&gt;
====AddonMessageOutputDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a message output plugin.&lt;br /&gt;
&lt;br /&gt;
====CoreBlockDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a block. As of Moodle App 3.7.0, blocks are only displayed in Site Home and Dashboard, but they&#039;ll be supported in other places of the app soon (e.g. in the course page).&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question types.&lt;br /&gt;
https://docs.moodle.org/dev/Creating_mobile_question_types&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionBehaviourDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question behaviours.&lt;br /&gt;
&lt;br /&gt;
====CoreUserProfileFieldDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting user profile fields.&lt;br /&gt;
&lt;br /&gt;
====AddonModQuizAccessRuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a quiz access rule.&lt;br /&gt;
&lt;br /&gt;
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use these delegates to support assign submission or feedback plugins.&lt;br /&gt;
&lt;br /&gt;
====AddonWorkshopAssessmentStrategyDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a workshop assessment strategy plugin.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.&lt;br /&gt;
&lt;br /&gt;
* CoreContentLinksDelegate&lt;br /&gt;
* CoreCourseModulePrefetchDelegate&lt;br /&gt;
* CoreFileUploaderDelegate&lt;br /&gt;
* CorePluginFileDelegate&lt;br /&gt;
&lt;br /&gt;
==Available components and directives==&lt;br /&gt;
&lt;br /&gt;
===Difference between component and directives===&lt;br /&gt;
&lt;br /&gt;
A component (represented as an HTML tag) is used to add custom elements to the app.&lt;br /&gt;
Example of components are: ion-list, ion-item, core-search-box&lt;br /&gt;
&lt;br /&gt;
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.&lt;br /&gt;
Example of directives are: core-auto-focus, *ngIf, ng-repeat&lt;br /&gt;
&lt;br /&gt;
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:&lt;br /&gt;
* Angular directives, please check: https://angular.io/api?type=directive&lt;br /&gt;
* Ionic components, please check: https://ionicframework.com/docs/&lt;br /&gt;
&lt;br /&gt;
===Custom core components and directives===&lt;br /&gt;
&lt;br /&gt;
These are some useful custom components and directives (only available in the mobile app). Please note that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.&lt;br /&gt;
&lt;br /&gt;
For a full list of components, go to https://github.com/moodlehq/moodlemobile2/tree/master/src/components&lt;br /&gt;
&lt;br /&gt;
For a full list of directives, go to https://github.com/moodlehq/moodlemobile2/tree/master/src/directives&lt;br /&gt;
&lt;br /&gt;
====core-format-text====&lt;br /&gt;
&lt;br /&gt;
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.&lt;br /&gt;
&lt;br /&gt;
This directive automatically applies core-external-content and core-link to all the links and embedded media.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;text&#039;&#039;&#039; (string): The text to format.&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* &#039;&#039;&#039;adaptImg&#039;&#039;&#039; (boolean): Optional. Whether to adapt images to screen width. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;clean&#039;&#039;&#039; (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;singleLine&#039;&#039;&#039; (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;maxHeight&#039;&#039;&#039; (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class=&amp;quot;inline&amp;quot; at the same time to use display: inline-block.&lt;br /&gt;
* &#039;&#039;&#039;fullOnClick&#039;&#039;&#039; (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;fullTitle&#039;&#039;&#039; (string): Optional. Title to use in full view. Defaults to &amp;quot;Description&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-format-text text=&amp;quot;&amp;lt;% cm.description %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-format-text&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-link====&lt;br /&gt;
&lt;br /&gt;
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;capture&#039;&#039;&#039; (boolean): Optional, default false. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).&lt;br /&gt;
* &#039;&#039;&#039;inApp&#039;&#039;&#039; (boolean): Optional, default false. True to open in embedded browser, false to open in system browser.&lt;br /&gt;
* &#039;&#039;&#039;autoLogin&#039;&#039;&#039; (string): Optional, default &amp;quot;check&amp;quot;. If the link should be open with auto-login. Accepts the following values:&lt;br /&gt;
** &amp;quot;yes&amp;quot; -&amp;gt; Always auto-login.&lt;br /&gt;
** &amp;quot;no&amp;quot; -&amp;gt; Never auto-login.&lt;br /&gt;
** &amp;quot;check&amp;quot; -&amp;gt; Auto-login only if it points to the current site. Default value.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a href=&amp;quot;&amp;lt;% cm.url %&amp;gt;&amp;quot; core-link&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-external-content====&lt;br /&gt;
&lt;br /&gt;
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. &lt;br /&gt;
&lt;br /&gt;
If a file is downloaded, its URL will be replaced by the local file URL.&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;img src=&amp;quot;&amp;lt;% event.iconurl %&amp;gt;&amp;quot; core-external-content component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% event.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-user-link====&lt;br /&gt;
&lt;br /&gt;
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;userId&#039;&#039;&#039; (number): User id to open the profile.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): Optional. Course id to show the user info related to that course.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a ion-item core-user-link userId=&amp;quot;&amp;lt;% userid %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-file====&lt;br /&gt;
&lt;br /&gt;
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* file (object): The file. Must have a property &#039;filename&#039; and a &#039;fileurl&#039; or &#039;url&#039;&lt;br /&gt;
* component (string): Optional. Component the file belongs to.&lt;br /&gt;
* componentId (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* canDelete (boolean): Optional. Whether file can be deleted.&lt;br /&gt;
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they&#039;re outdated or not.&lt;br /&gt;
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-file [file]=&amp;quot;{fileurl: &#039;&amp;lt;% issue.url %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.name %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, filesize: &#039;&amp;lt;% issue.size %&amp;gt;&#039;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-file&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-download-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.&lt;br /&gt;
&lt;br /&gt;
It is usually recommended to use the core-file component since it also displays the state of the file.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;core-download-file&#039;&#039;&#039; (object): The file to download.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage: a button to download a file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button [core-download-file]=&amp;quot;{fileurl: &amp;lt;% issue.url %&amp;gt;, timemodified: &amp;lt;% issue.timemodified %&amp;gt;, filesize: &amp;lt;% issue.size %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.download | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-course-download-module-main-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and opening the main file of a module.&lt;br /&gt;
&lt;br /&gt;
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.&lt;br /&gt;
&lt;br /&gt;
This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;module&#039;&#039;&#039; (object): Optional. The module object. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;moduleId&#039;&#039;&#039; (number): Optional. The module ID. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): The course ID the module belongs to.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.&lt;br /&gt;
* &#039;&#039;&#039;files&#039;&#039;&#039; (object[]): Optional. List of files of the module. If not provided, use module.contents.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-navbar-buttons====&lt;br /&gt;
&lt;br /&gt;
Component to add buttons to the app&#039;s header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.&lt;br /&gt;
&lt;br /&gt;
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first &amp;lt;ion-buttons&amp;gt; found in the header.&lt;br /&gt;
&lt;br /&gt;
You can use the [hidden] input to hide all the inner buttons if a certain condition is met.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons end&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button icon-only (click)=&amp;quot;action()&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;ion-icon name=&amp;quot;funnel&amp;quot;&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also use this to add options to the context menu. Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons&amp;gt;&lt;br /&gt;
    &amp;lt;core-context-menu&amp;gt;&lt;br /&gt;
        &amp;lt;core-context-menu-item [priority]=&amp;quot;500&amp;quot; [content]=&amp;quot;&#039;Nice boat&#039;&amp;quot; (action)=&amp;quot;boatFunction()&amp;quot; [iconAction]=&amp;quot;&#039;boat&#039;&amp;quot;&amp;gt;&amp;lt;/core-context-menu-item&amp;gt;&lt;br /&gt;
    &amp;lt;/core-context-menu&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.&lt;br /&gt;
&lt;br /&gt;
===Specific component and directives for plugins===&lt;br /&gt;
&lt;br /&gt;
These are component and directives created specifically for supporting Moodle plugins.&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherData]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the new &#039;&#039;get_content&#039;&#039; WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to go to a new content page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to load new content in current page using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.&lt;br /&gt;
&lt;br /&gt;
If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;successMessage&#039;&#039;&#039; (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).&lt;br /&gt;
* &#039;&#039;&#039;goBackOnSuccess&#039;&#039;&#039; (boolean): Whether to go back if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;refreshOnSuccess&#039;&#039;&#039; (boolean): Whether to refresh the current view if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server without using cache, displaying default messages and refreshing on success:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage successMessage refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; goBackOnSuccess=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;certificateViewed($event)&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.certificateViewed = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t need to load some new content when done, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;jsData&#039;&#039;&#039; (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;newContentPreSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server without using cache, showing default confirm and displaying a new page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server using cache, without confirm, displaying new content in same page and using &#039;&#039;userid&#039;&#039; from &#039;&#039;otherdata&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-on-load====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.&lt;br /&gt;
&lt;br /&gt;
If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Advanced features==&lt;br /&gt;
&lt;br /&gt;
===Display the plugin only if certain conditions are met===&lt;br /&gt;
&lt;br /&gt;
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the &amp;quot;init&amp;quot; method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).&lt;br /&gt;
&lt;br /&gt;
All the init methods are called as soon as your plugin is retrieved. If you don&#039;t want your plugin to be displayed for the current user, then you should throw an exception in this init method. It&#039;s recommended to include a message explaining why the plugin isn&#039;t available for the current user, this exception will be logged in the Javascript console.&lt;br /&gt;
&lt;br /&gt;
On the other hand, you might want to display a plugin only for certain courses (&#039;&#039;CoreCourseOptionsDelegate&#039;&#039;) or only if the user is viewing certain users&#039; profiles (&#039;&#039;CoreUserDelegate&#039;&#039;). This can be achieved with the init method too.&lt;br /&gt;
&lt;br /&gt;
In the init method you can return a &amp;quot;restrict&amp;quot; property with two fields in it: &#039;&#039;courses&#039;&#039; and &#039;&#039;users&#039;&#039;. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users&#039; profiles.&lt;br /&gt;
&lt;br /&gt;
===Using “otherdata”===&lt;br /&gt;
&lt;br /&gt;
The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a &#039;&#039;get_content&#039;&#039; WS call is added to a variable named CONTENT_OTHERDATA.&lt;br /&gt;
&lt;br /&gt;
The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.&lt;br /&gt;
&lt;br /&gt;
This means that, in your Javascript, you can access and use these data like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
this.CONTENT_OTHERDATA.myVar&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
And in the template you could use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
{{ CONTENT_OTHERDATA.myVar }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;myVar&#039;&#039; is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:&lt;br /&gt;
&lt;br /&gt;
array(&#039;myVar&#039; =&amp;gt; &#039;Initial value&#039;)&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.&lt;br /&gt;
&lt;br /&gt;
We will return the initial value of the input in the otherdata of our PHP method:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;myVar&#039; =&amp;gt; &#039;My initial value&#039;),&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then in the template we will use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-item text-wrap&amp;gt;&lt;br /&gt;
    &amp;lt;ion-label stacked&amp;gt;{{ &#039;plugin.mod_certificate.textlabel | translate }}&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;text&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.myVar&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [useOtherDataForWS]=&amp;quot;[&#039;myVar&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
        {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we are creating an input text and we use &#039;&#039;[(ngModel)]&#039;&#039; to use the value in &#039;&#039;myVar&#039;&#039; as the initial value and to store the changes in the same &#039;&#039;myVar&#039;&#039; variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the &#039;&#039;myVar&#039;&#039; variable. This is called 2-way data binding in Angular.&lt;br /&gt;
&lt;br /&gt;
Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the &#039;&#039;useOtherDataForWS&#039;&#039; attribute to specify which variable from &#039;&#039;otherdata&#039;&#039; we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService &#039;&#039;mod_certificate_my_webservice&#039;&#039; and will send as a param: myVar -&amp;gt; “A new value”.&lt;br /&gt;
&lt;br /&gt;
We can achieve the same result using the &#039;&#039;params&#039;&#039; attribute of the core-site-plugins-call-ws directive instead of using &#039;&#039;useOtherDataForWS&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [params]=&amp;quot;{myVar: CONTENT_OTHERDATA.myVar}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The WebService call will be exactly the same with both buttons.&lt;br /&gt;
&lt;br /&gt;
Please notice that this example could be done without using otherdata too, using the “&#039;&#039;form&#039;&#039;” input of the &#039;&#039;core-site-plugins-call-ws directive&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Running JS code after a content template has loaded===&lt;br /&gt;
&lt;br /&gt;
When you return JavaScript code from a handler function using the &#039;javascript&#039; array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. &lt;br /&gt;
&lt;br /&gt;
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;template&#039; =&amp;gt; [ ... ],&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;setTimeout(function() { console.log(&amp;quot;DOM is available now&amp;quot;); });&#039;,&lt;br /&gt;
    &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
    &#039;files&#039; =&amp;gt; []&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
===JS functions visible in the templates===&lt;br /&gt;
&lt;br /&gt;
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;openContent(title: string, args: any, component?: string, method?: string)&#039;&#039;&#039;: Open a new page to display some new content. You need to specify the &#039;&#039;title&#039;&#039; of the new page and the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
* &#039;&#039;&#039;refreshContent(showSpinner = true)&#039;&#039;&#039;: Refresh the current content. By default it will display a spinner while refreshing, if you don&#039;t want it to be displayed you should pass false as a parameter.&lt;br /&gt;
* &#039;&#039;&#039;updateContent(args: any, component?: string, method?: string)&#039;&#039;&#039;: Refresh the current content using different params. You need to specify the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Group selector=====&lt;br /&gt;
&lt;br /&gt;
Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.&lt;br /&gt;
&lt;br /&gt;
To do so, we&#039;ll use a drop down to select the group. When the user selects a group using this drop down we&#039;ll update the page content to display the new group.&lt;br /&gt;
&lt;br /&gt;
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular&#039;s &#039;&#039;ngModel&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
======Using plain HTML======&lt;br /&gt;
&lt;br /&gt;
We need to add a &amp;quot;&#039;&#039;selected&#039;&#039;&amp;quot; attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
        // Detect which group is selected.&lt;br /&gt;
        foreach ($groups as $gid=&amp;gt;$group) {&lt;br /&gt;
            $group-&amp;gt;selected = $gid === $groupid;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid,&lt;br /&gt;
            &#039;groups&#039; =&amp;gt; $groups&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the code above, we&#039;re retrieving the groups the user can see and then we&#039;re adding a &amp;quot;selected&amp;quot; bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.&lt;br /&gt;
&lt;br /&gt;
In the template, we display the drop down like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: $event})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot; &amp;lt;%#selected%&amp;gt;selected&amp;lt;%/selected%&amp;gt; &amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;ionChange&#039;&#039; function will be called everytime the user selects a different group with the drop down. We&#039;re using the function &#039;&#039;updateContent&#039;&#039; to update the current view using the new group. &#039;&#039;$event&#039;&#039; is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.&lt;br /&gt;
&lt;br /&gt;
======Using ngModel======&lt;br /&gt;
&lt;br /&gt;
ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn&#039;t have &#039;&#039;ng-init&#039;&#039; like in AngularJS), so we&#039;ll use &amp;quot;otherdata&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
In the PHP function we&#039;ll return the group that needs to be selected in the &#039;&#039;otherdata&#039;&#039; array:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
         return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; array(&lt;br /&gt;
                &#039;group&#039; =&amp;gt; $groupid&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we don&#039;t need to iterate over the groups array like in the plain HTML example. However, now we&#039;re returning the groupid in the &amp;quot;otherdata&amp;quot; array. As it&#039;s explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using &amp;quot;otherdata&amp;quot;]] section, this &amp;quot;otherdata&amp;quot; is visible in the templates inside a variable named &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. So in the template we&#039;ll use this variable like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.group&amp;quot; (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: CONTENT_OTHERDATA.group})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Use the rich text editor===&lt;br /&gt;
&lt;br /&gt;
The rich text editor included in the app requires a FormControl to work. You can use the library FormBuilder to create this control (or to create a whole FormGroup if you prefer).&lt;br /&gt;
&lt;br /&gt;
With the following Javascript you&#039;ll be able to create a FormControl:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we&#039;re using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.&lt;br /&gt;
&lt;br /&gt;
Then you need to pass this control to the rich text editor in your template:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;core-rich-text-editor item-content [control]=&amp;quot;control&amp;quot; placeholder=&amp;quot;Enter your text here&amp;quot; name=&amp;quot;rte_answer&amp;quot;&amp;gt;&amp;lt;/core-rich-text-editor&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, there are several ways to send the value in the rich text editor to a WebService to save it. This is one of the simplest options:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_webservice&amp;quot; [params]=&amp;quot;{rte: control.value}&amp;quot; ....&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re passing the value of the rich text editor as a parameter to our WebService.&lt;br /&gt;
&lt;br /&gt;
===Initialization===&lt;br /&gt;
&lt;br /&gt;
All handlers can specify a “&#039;&#039;init&#039;&#039;” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.&lt;br /&gt;
&lt;br /&gt;
When the app retrieves all the handlers, the first thing it will do is call the &#039;&#039;tool_mobile_get_content&#039;&#039; WebService with the init method. This WS call will only receive the default args.&lt;br /&gt;
&lt;br /&gt;
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.&lt;br /&gt;
&lt;br /&gt;
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.INIT_TEMPLATES[‘main’];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
In this case, “main” is the ID of the template we want to use.&lt;br /&gt;
&lt;br /&gt;
The same happens with the &#039;&#039;otherdata&#039;&#039; returned by this init method, it is added to a INIT_OTHERDATA variable.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;restrict&#039;&#039; field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate &#039;&#039;CoreCourseOptionsDelegate&#039;&#039; and you return a list of courseids in restrict-&amp;gt;courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.&lt;br /&gt;
&lt;br /&gt;
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    MyAddonClass: new MyAddonClass()&lt;br /&gt;
};&lt;br /&gt;
result:&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.MyAddonClass&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Module link handler=====&lt;br /&gt;
&lt;br /&gt;
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function AddonModCertificateModuleLinkHandler() {&lt;br /&gt;
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, &#039;mmaModCertificate&#039;, &#039;certificate&#039;);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateLinkHandler&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;&lt;br /&gt;
&lt;br /&gt;
this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Advanced link handler=====&lt;br /&gt;
Link handlers have some advanced features that allow you to change how links behave under different conditions.&lt;br /&gt;
======Patterns======&lt;br /&gt;
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.pattern = RegExp(&#039;\/mod\/foo\/specialpage.php&#039;);&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Priority======&lt;br /&gt;
Multiple link handlers may apply to a given link.  You can define the order of precedence by setting the priority - the handler with the highest priority will be used.&lt;br /&gt;
All default handlers have a priority of 0, so 1 or higher will override the default.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Multiple actions======&lt;br /&gt;
Once a link has been matched, the handler&#039;s getActions() method determines what the link should do.  This method has access to the URL and its parameters.&lt;br /&gt;
Different actions can be returned depending on different conditions.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {&lt;br /&gt;
    return [{&lt;br /&gt;
        action: function(siteId, navCtrl) {&lt;br /&gt;
            // The actual behaviour of the link goes here.&lt;br /&gt;
        },&lt;br /&gt;
        sites: [...]&lt;br /&gt;
    }, {&lt;br /&gt;
        ...&lt;br /&gt;
    }];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order.  The first &amp;quot;valid&amp;quot; action will be used to open the link.&lt;br /&gt;
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can &amp;quot;invalidate&amp;quot;&lt;br /&gt;
your action by settings its sites propety to an empty array.&lt;br /&gt;
======Complex example======&lt;br /&gt;
This will match all URLs containing /mod/foo/, and force those with an id parameter that&#039;s not in the &amp;quot;supportedModFoos&amp;quot; array to open in the user&#039;s browser, rather than the app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
var supportedModFoos = [...];&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    this.pattern = new RegExp(&#039;\/mod\/foo\/&#039;);&lt;br /&gt;
    this.name = &amp;quot;AddonModFooLinkHandler&amp;quot;;&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
}&lt;br /&gt;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);&lt;br /&gt;
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     &lt;br /&gt;
    var action = {&lt;br /&gt;
        action: function() {&lt;br /&gt;
            that.CoreUtilsProvider.openInBrowser(url);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {&lt;br /&gt;
        action.sites = [];&lt;br /&gt;
    }&lt;br /&gt;
    return [action];&lt;br /&gt;
};&lt;br /&gt;
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Module prefetch handler=====&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;CoreCourseModuleDelegate&#039;&#039; handler allows you to define a list of &#039;&#039;offlinefunctions&#039;&#039; to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using &#039;&#039;offlinefunctions&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to create a prefetch handler using init JS:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
// Create a class that &amp;quot;inherits&amp;quot; from CoreCourseActivityPrefetchHandlerBase.&lt;br /&gt;
function AddonModCertificateModulePrefetchHandler() {&lt;br /&gt;
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,&lt;br /&gt;
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateModulePrefetchHandler&amp;quot;;&lt;br /&gt;
    this.modName = &amp;quot;certificate&amp;quot;;&lt;br /&gt;
    this.component = &amp;quot;mod_certificate&amp;quot;; // This must match the plugin identifier from db/mobile.php, otherwise the download link in the context menu will not update correctly.&lt;br /&gt;
    this.updatesNames = /^configuration$|^.*files$/;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;&lt;br /&gt;
&lt;br /&gt;
// Override the prefetch call.&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {&lt;br /&gt;
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function prefetchCertificate(module, courseId, single, siteId) {&lt;br /&gt;
    // Perform all the WS calls.&lt;br /&gt;
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won&#039;t be able to prefetch all the possible parameters. &lt;br /&gt;
&lt;br /&gt;
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom &#039;section&#039; parameter for the mobile function &#039;mobile_document_view&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function prefetchOucontent(module, courseId, single, siteId) {&lt;br /&gt;
    var component = &#039;mod_oucontent&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Get the site, first.&lt;br /&gt;
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {&lt;br /&gt;
        // Read the list of pages in this document using a web service.&lt;br /&gt;
        return site.read(&#039;mod_oucontent_get_page_list&#039;, {&#039;cmid&#039;: module.id}).then(function(response) {&lt;br /&gt;
            var promises = [];&lt;br /&gt;
&lt;br /&gt;
            // For each page, read and process the page - this is a copy of logic in the app at&lt;br /&gt;
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.&lt;br /&gt;
            for(var i = 0; i &amp;lt; response.length; i++) {&lt;br /&gt;
                var args = {&lt;br /&gt;
                    courseid: courseId,&lt;br /&gt;
                    cmid: module.id,&lt;br /&gt;
                    userid: site.getUserId()&lt;br /&gt;
                };&lt;br /&gt;
                if (response[i] !== &#039;&#039;) {&lt;br /&gt;
                    args.section = response[i];&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                promises.push(that.CoreSitePluginsProvider.getContent(&lt;br /&gt;
                        component, &#039;mobile_document_view&#039;, args).then(&lt;br /&gt;
                        function(result) {&lt;br /&gt;
                            var subPromises = [];&lt;br /&gt;
                            if (result.files &amp;amp;&amp;amp; result.files.length) {&lt;br /&gt;
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(&lt;br /&gt;
                                        site.id, result.files, true, false, component, module.id));&lt;br /&gt;
                            }&lt;br /&gt;
                            return Promise.all(subPromises);&lt;br /&gt;
                        }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            return Promise.all(promises);&lt;br /&gt;
        });&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Single activity course format=====&lt;br /&gt;
&lt;br /&gt;
In the following example, the value of INIT_TEMPLATES[&amp;quot;main&amp;quot;] is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;core-dynamic-component [component]=&amp;quot;componentClass&amp;quot; [data]=&amp;quot;data&amp;quot;&amp;gt;&amp;lt;/core-dynamic-component&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This template is returned by the init method. And this is the JavaScript code returned by the init method:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function getAddonSingleActivityFormatComponent() {&lt;br /&gt;
    function AddonSingleActivityFormatComponent() {&lt;br /&gt;
        this.data = {};&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {&lt;br /&gt;
        var self = this;&lt;br /&gt;
&lt;br /&gt;
        if (this.course &amp;amp;&amp;amp; this.sections &amp;amp;&amp;amp; this.sections.length) {&lt;br /&gt;
            var module = this.sections[0] &amp;amp;&amp;amp; this.sections[0].modules &amp;amp;&amp;amp; this.sections[0].modules[0];&lt;br /&gt;
            if (module &amp;amp;&amp;amp; !this.componentClass) {&lt;br /&gt;
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) =&amp;gt; {&lt;br /&gt;
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.data.courseId = this.course.id;&lt;br /&gt;
            this.data.module = module;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {&lt;br /&gt;
        return Promise.resolve(this.dynamicComponent.callComponentFunction(&amp;quot;doRefresh&amp;quot;, [refresher, done]));&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
return AddonSingleActivityFormatComponent;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function AddonSingleActivityFormatHandler() {&lt;br /&gt;
    this.name = &amp;quot;singleactivity&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.isEnabled = function() {&lt;br /&gt;
    return true;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {&lt;br /&gt;
    if (sections &amp;amp;&amp;amp; sections[0] &amp;amp;&amp;amp; sections[0].modules &amp;amp;&amp;amp; sections[0].modules[0]) {&lt;br /&gt;
        return sections[0].modules[0].name;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return course.fullname || &amp;quot;&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {&lt;br /&gt;
    that.Injector = injector || that.Injector;&lt;br /&gt;
&lt;br /&gt;
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES[&amp;quot;main&amp;quot;], getAddonSingleActivityFormatComponent(), injector);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using the JavaScript API===&lt;br /&gt;
&lt;br /&gt;
The Javascript API is partly supported right now, only the delegates specified in the section [[Mobile_support_for_plugins#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] supports it now. This API allows you to override any of the functions of the default handler. &lt;br /&gt;
&lt;br /&gt;
The “method” specified in a handler registered in the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.&lt;br /&gt;
&lt;br /&gt;
For example, if the Javascript returned by the method returns something like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The the &#039;&#039;getData&#039;&#039; function of the default handler will be overridden by the returned getData function.&lt;br /&gt;
&lt;br /&gt;
The default handler for &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; only has 2 functions: &#039;&#039;getComponent&#039;&#039; and &#039;&#039;getData&#039;&#039;. In addition, the JavaScript code can return an extra function named &#039;&#039;componentInit&#039;&#039; that will be executed when the component returned by &#039;&#039;getComponent&#039;&#039; is initialized.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to support the text user profile field using this API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
var result = {&lt;br /&gt;
    componentInit: function() {&lt;br /&gt;
        if (this.field &amp;amp;&amp;amp; this.edit &amp;amp;&amp;amp; this.form) {&lt;br /&gt;
            this.field.modelName = &amp;quot;profile_field_&amp;quot; + this.field.shortname;&lt;br /&gt;
&lt;br /&gt;
            if (this.field.param2) {&lt;br /&gt;
                this.field.maxlength = parseInt(this.field.param2, 10) || &amp;quot;&amp;quot;;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? &amp;quot;password&amp;quot; : &amp;quot;text&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var formData = {&lt;br /&gt;
                value: this.field.defaultdata,&lt;br /&gt;
                disabled: this.disabled&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required &amp;amp;&amp;amp; !this.field.locked ? that.Validators.required : null));&lt;br /&gt;
        }&lt;br /&gt;
    },&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
        var name = &amp;quot;profile_field_&amp;quot; + field.shortname;&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            type: &amp;quot;text&amp;quot;,&lt;br /&gt;
            name: name,&lt;br /&gt;
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name])&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Translate dynamic strings===&lt;br /&gt;
&lt;br /&gt;
If you wish to have an element that displays a localised string based on value from your template you can doing something like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content translate&amp;gt;&lt;br /&gt;
        plugin.mod_myactivity.&amp;lt;% status %&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This could save you from having to write something like when only one value should be displayed:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content&amp;gt;&lt;br /&gt;
        &amp;lt;%#isedting%&amp;gt;{{ &#039;plugin.mod_myactivity.editing&#039; | translate }}&amp;lt;%/isediting%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isopen%&amp;gt;{{ &#039;plugin.mod_myactivity.open&#039; | translate }}&amp;lt;%/isopen%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isclosed%&amp;gt;{{ &#039;plugin.mod_myactivity.closed&#039; | translate }}&amp;lt;%/isclosed%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using strings with dates===&lt;br /&gt;
&lt;br /&gt;
If you have a string that you wish to pass a formatted date for example in the Moodle language file you have:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;strwithdate&#039;] = &#039;This string includes a date of {$a-&amp;gt;date} in the middle of it.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can localise the string correctly in your template using something like the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{{ &#039;plugin.mod_myactivity.strwithdate&#039; | translate: {$a: { date: &amp;lt;% timestamp %&amp;gt; * 1000 | coreFormatDate: &amp;quot;dffulldate&amp;quot; } } }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, where as Unix timestamps are in seconds.&lt;br /&gt;
&lt;br /&gt;
===Support push notification clicks===&lt;br /&gt;
&lt;br /&gt;
If your plugin sends push notifications to the app, you might want to open a certain page in the app when the notification is clicked. There are several ways to achieve this.&lt;br /&gt;
&lt;br /&gt;
The easiest way is to include a &#039;&#039;contexturl&#039;&#039; in your notification. When the notification is clicked, the app will try to open the &#039;&#039;contexturl&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &#039;&#039;contexturl&#039;&#039; will also be displayed in web. If you want to use a specific URL for the app, different than the one displayed in web, you can do so by returning a &#039;&#039;customdata&#039;&#039; array that contains an &#039;&#039;appurl&#039;&#039; property:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$notification-&amp;gt;customdata = [&lt;br /&gt;
    &#039;appurl&#039; =&amp;gt; $myurl-&amp;gt;out(),&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In both cases you will have to create a link handler to treat the URL. For more info on how to create the link handler, please see [[Mobile_support_for_plugins#Advanced_link_handler|how to create an advanced link handler]].&lt;br /&gt;
&lt;br /&gt;
If you want to do something that only happens when the notification is clicked, not when the link is clicked, you&#039;ll have to implement a push click handler yourself. The way to create it is similar to [[Mobile_support_for_plugins#Advanced_link_handler|creating an advanced link handler]], but you&#039;ll have to use &#039;&#039;CorePushNotificationsDelegate&#039;&#039; and your handler will have to implement the properties and functions defined in the interface [https://github.com/moodlehq/moodlemobile2/blob/master/src/core/pushnotifications/providers/delegate.ts#L24 CorePushNotificationsClickHandler].&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
=== Invalid response received ===&lt;br /&gt;
&lt;br /&gt;
You might receive this error when using the &amp;quot;core-site-plugins-call-ws&amp;quot; directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{typeExpected: &#039;boolean&#039;}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{responseExpected: false}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS ===&lt;br /&gt;
&lt;br /&gt;
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don&#039;t use HTML inputs, they simulate them, so the directive isn&#039;t going to find their data and so it won&#039;t be sent to the WebService.&lt;br /&gt;
&lt;br /&gt;
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.&lt;br /&gt;
&lt;br /&gt;
==== Sending the data manually ====&lt;br /&gt;
&lt;br /&gt;
The first solution is to send the missing params manually using the &amp;quot;&#039;&#039;params&#039;&#039;&amp;quot; property. We will use &#039;&#039;ngModel&#039;&#039; to store the input value in a variable, and this variable will be passed to the params. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a template like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group name=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, responses: responses}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Basically, you need to add &#039;&#039;ngModel&#039;&#039; to the affected element (in this case, the &#039;&#039;radio-group&#039;&#039;). You can put whatever name you want as the value, we used &amp;quot;responses&amp;quot;. With this, everytime the user selects a radio button the value will be stored in a variable named &amp;quot;responses&amp;quot;. Then, in the button we are passing this variable to the params of the WebService.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &amp;quot;form&amp;quot; attribute has priority over &amp;quot;params&amp;quot;, so if you have an input with name=&amp;quot;responses&amp;quot; it will override what you&#039;re manually passing to params.&lt;br /&gt;
&lt;br /&gt;
==== Using a hidden input ====&lt;br /&gt;
&lt;br /&gt;
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use &#039;&#039;ngModel&#039;&#039; to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a radio button like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot; [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;hidden&amp;quot; [ngModel]=&amp;quot;responses&amp;quot; name=&amp;quot;responses&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we&#039;re using a variable named &amp;quot;responses&amp;quot; to synchronize the data between the &#039;&#039;radio-group&#039;&#039; and the hidden input. You can use whatever name you want.&lt;br /&gt;
&lt;br /&gt;
=== I can&#039;t return an object or array in otherdata ===&lt;br /&gt;
&lt;br /&gt;
If you try to return an object or an array in any field inside &#039;&#039;otherdata&#039;&#039;, the WebService call will fail with the following error:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Scalar type expected, array or object received&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Each field in &#039;&#039;otherdata&#039;&#039; must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($data))&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The app will automatically parse this JSON and convert it back into an array or object.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===Accepting dynamic names in a WebService===&lt;br /&gt;
&lt;br /&gt;
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.&lt;br /&gt;
&lt;br /&gt;
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they&#039;re dynamic. The solution is to accept an array of objects with name and value. So in the &#039;&#039;_parameters()&#039;&#039; function of our new WebService, we will add this parameter:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;data&#039; =&amp;gt; new external_multiple_structure(&lt;br /&gt;
     new external_single_structure(&lt;br /&gt;
        array(&lt;br /&gt;
            &#039;name&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data name&#039;),&lt;br /&gt;
            &#039;value&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data value&#039;),&lt;br /&gt;
        )&lt;br /&gt;
    ),&lt;br /&gt;
    &#039;The data to be saved&#039;, VALUE_DEFAULT, array()&lt;br /&gt;
)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive &#039;&#039;core-site-plugins-call-ws&#039;&#039; that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the &amp;quot;&#039;&#039;form&#039;&#039;&amp;quot; attribute, because we need to format the data before it is sent.&lt;br /&gt;
&lt;br /&gt;
Since we will send the params manually and we want it all to be sent in the same array, we will use &#039;&#039;ngModel&#039;&#039; to store the input data into a variable that we&#039;ll call &amp;quot;data&amp;quot;, but you can use the name you want. This &amp;quot;data&amp;quot; will be an object that will hold the input data with the format &amp;quot;name-&amp;gt;value&amp;quot;. For example, if I have an input with name &amp;quot;a1&amp;quot; and value &amp;quot;My answer&amp;quot;, the data object will be:&lt;br /&gt;
&lt;br /&gt;
{a1: &amp;quot;My answer&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
So we need to add &#039;&#039;ngModel&#039;&#039; to all the inputs whose values need to be sent to the &amp;quot;data&amp;quot; WS param. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&amp;lt;ion-input name=&amp;quot;&amp;lt;% name %&amp;gt;&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.data[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re using &#039;&#039;CONTENT_OTHERDATA&#039;&#039; to store the data. We do it like this because we&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the form, setting the values the user has already stored. If you don&#039;t need to initialize the form, then you can use the variable &amp;quot;dataObject&amp;quot;, an empty object that the Mobile app creates for you: [(ngModel)]=&amp;quot;dataObject[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: &#039;&#039;objectToArrayOfObjects&#039;&#039;. So in our button we&#039;ll use this function to format the data before it&#039;s sent:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_ws_name&amp;quot;&lt;br /&gt;
    [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, &#039;name&#039;, &#039;value&#039;)}&amp;quot;&lt;br /&gt;
    successMessage&lt;br /&gt;
    refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see in the example above, we&#039;re specifying that the keys of the &amp;quot;data&amp;quot; object need to be stored in a property named &amp;quot;name&amp;quot;, and the values need to be stored in a property named &amp;quot;value&amp;quot;. If your WebService expects different names you need to change the parameters of the function &#039;&#039;objectToArrayOfObjects&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable &amp;quot;data&amp;quot; doesn&#039;t exist inside &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. As it is explained in previous sections, &#039;&#039;CONTENT_OTHERDATA&#039;&#039; holds the data that you return in &#039;&#039;otherdata&#039;&#039; for your method. We&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the values to be displayed in the form.&lt;br /&gt;
&lt;br /&gt;
If the user hasn&#039;t answered the form yet, we can initialize the &amp;quot;data&amp;quot; object as an empty object. Please remember that we cannot return arrays or objects in &#039;&#039;otherdata&#039;&#039;, so we&#039;ll return a JSON string.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; &#039;{}&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$userdata = get_user_responses(); // It will held the data in a format name-&amp;gt;value. Example: array(&#039;a1&#039; =&amp;gt; &#039;My value&#039;).&lt;br /&gt;
...&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($userdata))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.&lt;br /&gt;
&lt;br /&gt;
==Moodle plugins with mobile support==&lt;br /&gt;
&lt;br /&gt;
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].&lt;br /&gt;
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].&lt;br /&gt;
* Gapfill question type: [https://moodle.org/plugins/qtype_gapfill Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_gapfill in github].&lt;br /&gt;
* Wordselect question type: [https://moodle.org/plugins/qtype_wordselect Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_wordselect in github].&lt;br /&gt;
* RegExp question type: [https://moodle.org/plugins/qtype_regexp Moodle plugins directory entry] and [https://github.com/rezeau/moodle-qtype_regexp in github].&lt;br /&gt;
* Certificate: [https://moodle.org/plugins/mod_certificate Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_certificate in github].&lt;br /&gt;
* Attendance [https://moodle.org/plugins/mod_attendance Moodle plugins directory entry] and [https://github.com/danmarsden/moodle-mod_attendance in github].&lt;br /&gt;
* ForumNG (unfinished support) [https://moodle.org/plugins/mod_forumng Moodle plugins directory entry] and [https://github.com/moodleou/moodle-mod_forumng in github].&lt;br /&gt;
* News block [https://github.com/moodleou/moodle-block_news in github].&lt;br /&gt;
* H5P activity module  [https://moodle.org/plugins/mod_hvp Moodle plugins directory entry] and [https://github.com/h5p/h5p-moodle-plugin in github].&lt;br /&gt;
&lt;br /&gt;
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&amp;amp;id=6 here] (it may contain some outdated plugins)&lt;br /&gt;
[[Category:Mobile]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55755</id>
		<title>Moodle App Plugins Development Guide</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55755"/>
		<updated>2019-03-18T16:12:35Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Advanced features */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
==Before 3.5==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle Mobile Remote add-ons|Remote add-ons]] for details.&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5 the app switched to a new way to suport plugins that was much easier for developers.&lt;br /&gt;
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).&lt;br /&gt;
* The use of JavaScript is optional (but some type of advanced plugins may require it)&lt;br /&gt;
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).&lt;br /&gt;
&lt;br /&gt;
This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)&lt;br /&gt;
&lt;br /&gt;
This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.&lt;br /&gt;
&lt;br /&gt;
==How it works==&lt;br /&gt;
&lt;br /&gt;
The overall idea is to allow Moodle plugins to extend different areas in the app with &#039;&#039;just PHP server side&#039;&#039; code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.&lt;br /&gt;
&lt;br /&gt;
Developers will have to:&lt;br /&gt;
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.&lt;br /&gt;
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates. &lt;br /&gt;
&lt;br /&gt;
Let’s clarify some points:&lt;br /&gt;
&lt;br /&gt;
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.&lt;br /&gt;
* Those functions will be exported via the Web Service function tool_mobile_get_content&lt;br /&gt;
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).&lt;br /&gt;
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..&lt;br /&gt;
&lt;br /&gt;
==Types of plugins==&lt;br /&gt;
&lt;br /&gt;
We could classify all the plugins in 3 different types:&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_when_requested.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you&#039;re developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_on_login.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
==Step by step example==&lt;br /&gt;
&lt;br /&gt;
In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.&lt;br /&gt;
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.&lt;br /&gt;
&lt;br /&gt;
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)&lt;br /&gt;
&lt;br /&gt;
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.&lt;br /&gt;
&lt;br /&gt;
===Step 1. Update the db/mobile.php file===&lt;br /&gt;
In this case, we are updating an existing file but for new plugins, you should create this new file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$addons = [&lt;br /&gt;
    &#039;mod_certificate&#039; =&amp;gt; [ // Plugin identifier&lt;br /&gt;
        &#039;handlers&#039; =&amp;gt; [ // Different places where the plugin will display content.&lt;br /&gt;
            &#039;coursecertificate&#039; =&amp;gt; [ // Handler unique name (alphanumeric).&lt;br /&gt;
                &#039;displaydata&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;icon&#039; =&amp;gt; $CFG-&amp;gt;wwwroot . &#039;/mod/certificate/pix/icon.gif&#039;,&lt;br /&gt;
                    &#039;class&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
                ],&lt;br /&gt;
       &lt;br /&gt;
                &#039;delegate&#039; =&amp;gt; &#039;CoreCourseModuleDelegate&#039;, // Delegate (where to display the link to the plugin)&lt;br /&gt;
                &#039;method&#039; =&amp;gt; &#039;mobile_course_view&#039;, // Main function in \mod_certificate\output\mobile&lt;br /&gt;
                &#039;offlinefunctions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;mobile_course_view&#039; =&amp;gt; [],&lt;br /&gt;
                    &#039;mobile_issues_view&#039; =&amp;gt; [],&lt;br /&gt;
                ]. // Function that needs to be downloaded for offline.&lt;br /&gt;
            ],&lt;br /&gt;
        ],&lt;br /&gt;
        &#039;lang&#039; =&amp;gt; [ // Language strings that are used in all the handlers.&lt;br /&gt;
            [&#039;pluginname&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;summaryofattempts&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;getcertificate&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;requiredtimenotmet&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;viewcertificateviews&#039;, &#039;certificate&#039;],&lt;br /&gt;
        ],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Plugin identifier:&lt;br /&gt;
: A unique name for the plugin, it can be anything (there’s no need to match the module name).&lt;br /&gt;
 &lt;br /&gt;
;Handlers  (Different places where the plugin will display content):&lt;br /&gt;
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).&lt;br /&gt;
&lt;br /&gt;
; Display data:&lt;br /&gt;
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.&lt;br /&gt;
	&lt;br /&gt;
; Delegate&lt;br /&gt;
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.&lt;br /&gt;
&lt;br /&gt;
; Method:&lt;br /&gt;
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.	&lt;br /&gt;
&lt;br /&gt;
; Offlinefunctions&lt;br /&gt;
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file. &lt;br /&gt;
: In our example, downloading for offline access will mean that we&#039;ll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we&#039;ll be able to display the certificate information even if the user is offline.&lt;br /&gt;
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).&lt;br /&gt;
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)&lt;br /&gt;
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the &#039;&#039;files&#039;&#039; array).&lt;br /&gt;
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module&#039;s view function by using a &#039;page&#039; parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].&lt;br /&gt;
&lt;br /&gt;
;Lang:&lt;br /&gt;
: The language pack string ids used in the plugin by all the handlers. Please note that you should avoid adding all the plugin string ids (including those unused) because the Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform.&lt;br /&gt;
&lt;br /&gt;
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.&lt;br /&gt;
&lt;br /&gt;
===Step 2. Creating the main function===&lt;br /&gt;
&lt;br /&gt;
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.&lt;br /&gt;
&lt;br /&gt;
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.&lt;br /&gt;
&lt;br /&gt;
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
namespace mod_certificate\output;&lt;br /&gt;
&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
&lt;br /&gt;
use context_module;&lt;br /&gt;
use mod_certificate_external;&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Mobile output class for certificate&lt;br /&gt;
 *&lt;br /&gt;
 * @package    mod_certificate&lt;br /&gt;
 * @copyright  2018 Juan Leyva&lt;br /&gt;
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later&lt;br /&gt;
 */&lt;br /&gt;
class mobile {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate course view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_course_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Set timemodified for each certificate.&lt;br /&gt;
        foreach ($issues as $issue) {&lt;br /&gt;
            if (empty($issue-&amp;gt;timemodified)) {&lt;br /&gt;
                    $issue-&amp;gt;timemodified = $issue-&amp;gt;timecreated;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $showget = true;&lt;br /&gt;
        if ($certificate-&amp;gt;requiredtime &amp;amp;&amp;amp; !has_capability(&#039;mod/certificate:manage&#039;, $context)) {&lt;br /&gt;
            if (certificate_get_course_time($certificate-&amp;gt;course) &amp;lt; ($certificate-&amp;gt;requiredtime * 60)) {&lt;br /&gt;
                    $showget = false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $certificate-&amp;gt;name = format_string($certificate-&amp;gt;name);&lt;br /&gt;
        list($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat) =&lt;br /&gt;
                        external_format_text($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat, $context-&amp;gt;id,&#039;mod_certificate&#039;, &#039;intro&#039;);&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;certificate&#039; =&amp;gt; $certificate,&lt;br /&gt;
            &#039;showget&#039; =&amp;gt; $showget &amp;amp;&amp;amp; count($issues) &amp;gt; 0,&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues,&lt;br /&gt;
            &#039;issue&#039; =&amp;gt; $issues[0],&lt;br /&gt;
            &#039;numissues&#039; =&amp;gt; count($issues),&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;files&#039; =&amp;gt; $issues,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let’s go through the function code to analyse the different parts.&lt;br /&gt;
&lt;br /&gt;
;Function declaration: &lt;br /&gt;
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument “$args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)&lt;br /&gt;
&lt;br /&gt;
; Function implementation:&lt;br /&gt;
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.&lt;br /&gt;
&lt;br /&gt;
Finally, we return:&lt;br /&gt;
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.&lt;br /&gt;
* JavaScript: Empty, because we don’t need any in this case&lt;br /&gt;
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.&lt;br /&gt;
* Files: A list of files that the app should be able to download (for offline usage mostly)&lt;br /&gt;
&lt;br /&gt;
===Step 3. Creating the template for the main function===&lt;br /&gt;
&lt;br /&gt;
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.&lt;br /&gt;
&lt;br /&gt;
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.&lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/ &lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with &#039;&#039;core-&#039;&#039; are custom components of the Mobile app.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_page.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;core-course-module-description description=&amp;quot;&amp;lt;% certificate.intro %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-course-module-description&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;ion-list-header&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-list-header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
                    {{ &#039;plugin.mod_certificate.viewcertificateviews&#039; | translate: {$a: &amp;lt;% numissues %&amp;gt;} }}&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;ion-icon name=&amp;quot;cloud-download&amp;quot; item-start&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
                {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%^showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;p&amp;gt;{{ &#039;plugin.mod_certificate.requiredtimenotmet&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;!-- Call log WS when the template is loaded. --&amp;gt;&lt;br /&gt;
        &amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Then we display the module description using &amp;lt;code&amp;gt;&amp;lt;core-course-module-description&amp;lt;/code&amp;gt; that is a component used to include the course module description.&lt;br /&gt;
&lt;br /&gt;
For displaying the certificate information we create a list of elements, adding a header on top.&lt;br /&gt;
The following line &amp;lt;code&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/code&amp;gt; indicates that the Mobile app will translate the &#039;&#039;summaryofattempts&#039;&#039; string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format: &lt;br /&gt;
&lt;br /&gt;
“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php). &lt;br /&gt;
&lt;br /&gt;
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) &amp;lt;code&amp;gt;core-site-plugins-new-content&amp;lt;/code&amp;gt; indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).&lt;br /&gt;
&lt;br /&gt;
Just after this button we display another one but this time for downloading an issued certificate. The &amp;lt;code&amp;gt;core-course-download-module-main-file&amp;lt;/code&amp;gt; directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.&lt;br /&gt;
&lt;br /&gt;
Finally, just before the ion-list is closed, we use the &amp;lt;code&amp;gt;core-site-plugins-call-ws-on-load&amp;lt;/code&amp;gt; directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the &#039;&#039;mod_certificate_view_certificate&#039;&#039; that will log that the user viewed this page.&lt;br /&gt;
&lt;br /&gt;
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.&lt;br /&gt;
&lt;br /&gt;
===Step 4. Adding an additional page===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Partial file contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate issues view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_issues_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = [&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_issues&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.&lt;br /&gt;
&lt;br /&gt;
The code of the mustache template is also very simple:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_issues.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &amp;lt;%timecreated%&amp;gt; | coreToLocaleString }}&amp;lt;/p&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;&amp;lt;%grade%&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Here we are creating an ionic list that will display a new item in the list per each issued certificated.&lt;br /&gt;
&lt;br /&gt;
For the issued certificated we’ll display the time when it was created (using the app filter &#039;&#039;coreToLocaleString&#039;&#039;). We are also displaying the grade displayed in the certificate (if any).&lt;br /&gt;
&lt;br /&gt;
===Step 5. Plugin webservices, if included===&lt;br /&gt;
&lt;br /&gt;
If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.&lt;br /&gt;
&lt;br /&gt;
The following line &amp;lt;code&amp;gt;&#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&amp;lt;/code&amp;gt; should be included in each webservice definition.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/db/services.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$functions = [&lt;br /&gt;
&lt;br /&gt;
    &#039;mod_certificate_get_certificates_by_courses&#039; =&amp;gt; [&lt;br /&gt;
        &#039;classname&#039;     =&amp;gt; &#039;mod_certificate_external&#039;,&lt;br /&gt;
        &#039;methodname&#039;    =&amp;gt; &#039;get_certificates_by_courses&#039;,&lt;br /&gt;
        &#039;description&#039;   =&amp;gt; &#039;Returns a list of certificate instances...&#039;,&lt;br /&gt;
        &#039;type&#039;          =&amp;gt; &#039;read&#039;,&lt;br /&gt;
        &#039;capabilities&#039;  =&amp;gt; &#039;mod/certificate:view&#039;,&lt;br /&gt;
        &#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
	&lt;br /&gt;
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.&lt;br /&gt;
&lt;br /&gt;
==Getting started==&lt;br /&gt;
&lt;br /&gt;
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!&lt;br /&gt;
&lt;br /&gt;
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app.&lt;br /&gt;
&lt;br /&gt;
Please test that your site works correctly in the web version before starting any development.&lt;br /&gt;
&lt;br /&gt;
===Moodle version requirements===&lt;br /&gt;
&lt;br /&gt;
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin]. &lt;br /&gt;
&lt;br /&gt;
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).&lt;br /&gt;
&lt;br /&gt;
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.&lt;br /&gt;
&lt;br /&gt;
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.&lt;br /&gt;
&lt;br /&gt;
===Development workflow===&lt;br /&gt;
&lt;br /&gt;
First of all, we recommend creating a simple &#039;&#039;mobile.php&#039;&#039; for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Check that you can correctly  see the new menu option you included.&lt;br /&gt;
&lt;br /&gt;
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.&lt;br /&gt;
&lt;br /&gt;
It is important to remember that:&lt;br /&gt;
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).&lt;br /&gt;
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).&lt;br /&gt;
&lt;br /&gt;
===Testing and debugging===&lt;br /&gt;
&lt;br /&gt;
To learn how to debug with the web version of the app, please read the following documents:&lt;br /&gt;
* [[Moodle Mobile debugging WS requests]] AND&lt;br /&gt;
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)&lt;br /&gt;
&lt;br /&gt;
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.&lt;br /&gt;
&lt;br /&gt;
Within the app, make sure to turn on the option: &#039;&#039;&#039;App settings&#039;&#039;&#039; / &#039;&#039;&#039;General&#039;&#039;&#039; / &#039;&#039;&#039;Display debug messages&#039;&#039;&#039;. This means popup errors from the app will show more information.&lt;br /&gt;
&lt;br /&gt;
==Mobile.php supported options==&lt;br /&gt;
&lt;br /&gt;
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:&lt;br /&gt;
&lt;br /&gt;
===Common options===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;delegate&#039;&#039;&#039; (mandatory): Name of the delegate to register the handler in.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (mandatory): The function to call to retrieve the main page content.&lt;br /&gt;
* &#039;&#039;&#039;init&#039;&#039;&#039; (optional): A function to call to retrieve the initialization JS and the &amp;quot;restrict&amp;quot; to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.&lt;br /&gt;
* &#039;&#039;&#039;restricttocurrentuser&#039;&#039;&#039; (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;restricttoenrolledcourses&#039;&#039;&#039; (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;styles&#039;&#039;&#039; (optional): An array with two properties: &#039;&#039;url&#039;&#039; and &#039;&#039;version&#039;&#039;. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It&#039;s recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseOptionsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreMainMenuDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the &amp;quot;More&amp;quot; tab, they cannot be displayed as tabs in the bottom bar.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseModuleDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): icon, class.&lt;br /&gt;
* &#039;&#039;&#039;offlinefunctions&#039;&#039;&#039;: (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + &#039;id&#039; (e.g. certificateid).&lt;br /&gt;
* &#039;&#039;&#039;downloadbutton&#039;&#039;&#039;: (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.&lt;br /&gt;
* &#039;&#039;&#039;isresource&#039;&#039;&#039;: (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the &amp;quot;contents&amp;quot; field, then it should be true.&lt;br /&gt;
* &#039;&#039;&#039;updatesnames&#039;&#039;&#039;: (optional) Only used if there is any offlinefunction. A Regular Expression to check if there&#039;s any update in the module. It will be compared to the result of &#039;&#039;core_course_check_updates&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;displayopeninbrowser&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Open in browser&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;&lt;br /&gt;
* &#039;&#039;&#039;displaydescription&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Description&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;&lt;br /&gt;
* &#039;&#039;&#039;displayrefresh&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Refresh&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;&lt;br /&gt;
* &#039;&#039;&#039;displayprefetch&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;&lt;br /&gt;
* &#039;&#039;&#039;displaysize&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseFormatDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;canviewallsections&#039;&#039;&#039;: (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displayenabledownload&#039;&#039;&#039;: (optional) Whether the option to enable section/module download should be displayed. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displaysectionselector&#039;&#039;&#039;: (optional) Whether the default section selector should be displayed. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreUserDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039;: The type of the addon. Values accepted: &#039;newpage&#039; (default) or  &#039;communication&#039;. &lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreSettingsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for AddonMessageOutputDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
==Delegates==&lt;br /&gt;
&lt;br /&gt;
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
====CoreMainMenuDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). &lt;br /&gt;
&lt;br /&gt;
====CoreCourseOptionsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).&lt;br /&gt;
&lt;br /&gt;
====CoreCourseModuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting activity modules or resources.&lt;br /&gt;
&lt;br /&gt;
====CoreUserDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add additional options in the user profile page in the app.&lt;br /&gt;
&lt;br /&gt;
====CoreCourseFormatDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting course formats.&lt;br /&gt;
&lt;br /&gt;
====CoreSettingsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to add a new option in the settings page.&lt;br /&gt;
&lt;br /&gt;
====AddonMessageOutputDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a message output plugin.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question types.&lt;br /&gt;
https://docs.moodle.org/dev/Creating_mobile_question_types&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionBehaviourDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question behaviours.&lt;br /&gt;
&lt;br /&gt;
====CoreUserProfileFieldDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting user profile fields.&lt;br /&gt;
&lt;br /&gt;
====AddonModQuizAccessRuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a quiz access rule.&lt;br /&gt;
&lt;br /&gt;
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use these delegates to support assign submission or feedback plugins.&lt;br /&gt;
&lt;br /&gt;
====AddonWorkshopAssessmentStrategyDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a workshop assessment strategy plugin.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.&lt;br /&gt;
&lt;br /&gt;
* CoreContentLinksDelegate&lt;br /&gt;
* CoreCourseModulePrefetchDelegate&lt;br /&gt;
* CoreFileUploaderDelegate&lt;br /&gt;
* CorePluginFileDelegate&lt;br /&gt;
&lt;br /&gt;
==Available components and directives==&lt;br /&gt;
&lt;br /&gt;
===Difference between component and directives===&lt;br /&gt;
&lt;br /&gt;
A component (represented as an HTML tag) is used to add custom elements to the app.&lt;br /&gt;
Example of components are: ion-list, ion-item, core-search-box&lt;br /&gt;
&lt;br /&gt;
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.&lt;br /&gt;
Example of directives are: core-auto-focus, *ngIf, ng-repeat&lt;br /&gt;
&lt;br /&gt;
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:&lt;br /&gt;
* Angular directives, please check: https://angular.io/api?type=directive&lt;br /&gt;
* Ionic components, please check: https://ionicframework.com/docs/&lt;br /&gt;
&lt;br /&gt;
===Custom core components and directives===&lt;br /&gt;
&lt;br /&gt;
These are some useful custom components and directives (only available in the mobile app). Please notice that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.&lt;br /&gt;
&lt;br /&gt;
====core-format-text====&lt;br /&gt;
&lt;br /&gt;
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.&lt;br /&gt;
&lt;br /&gt;
This directive automatically applies core-external-content and core-link to all the links and embedded media.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;text&#039;&#039;&#039; (string): The text to format.&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* &#039;&#039;&#039;adaptImg&#039;&#039;&#039; (boolean): Optional. Whether to adapt images to screen width. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;clean&#039;&#039;&#039; (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;singleLine&#039;&#039;&#039; (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;maxHeight&#039;&#039;&#039; (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class=&amp;quot;inline&amp;quot; at the same time to use display: inline-block.&lt;br /&gt;
* &#039;&#039;&#039;fullOnClick&#039;&#039;&#039; (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;fullTitle&#039;&#039;&#039; (string): Optional. Title to use in full view. Defaults to &amp;quot;Description&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-format-text text=&amp;quot;&amp;lt;% cm.description %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-format-text&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-link====&lt;br /&gt;
&lt;br /&gt;
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;capture&#039;&#039;&#039; (boolean): Optional. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).&lt;br /&gt;
* &#039;&#039;&#039;inApp&#039;&#039;&#039; (boolean): Optional. True to open in embedded browser, false to open in system browser.&lt;br /&gt;
* &#039;&#039;&#039;autoLogin&#039;&#039;&#039; (string): Optional. If the link should be open with auto-login. Accepts the following values:&lt;br /&gt;
** &amp;quot;yes&amp;quot; -&amp;gt; Always auto-login.&lt;br /&gt;
** &amp;quot;no&amp;quot; -&amp;gt; Never auto-login.&lt;br /&gt;
** &amp;quot;check&amp;quot; -&amp;gt; Auto-login only if it points to the current site. Default value.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a href=&amp;quot;&amp;lt;% cm.url %&amp;gt;&amp;quot; core-link&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-external-content====&lt;br /&gt;
&lt;br /&gt;
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. &lt;br /&gt;
&lt;br /&gt;
If a file is downloaded, its URL will be replaced by the local file URL.&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;img src=&amp;quot;&amp;lt;% event.iconurl %&amp;gt;&amp;quot; core-external-content component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% event.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-user-link====&lt;br /&gt;
&lt;br /&gt;
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;userId&#039;&#039;&#039; (number): User id to open the profile.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): Optional. Course id to show the user info related to that course.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a ion-item core-user-link userId=&amp;quot;&amp;lt;% userid %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-file====&lt;br /&gt;
&lt;br /&gt;
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* file (object): The file. Must have a property &#039;filename&#039; and a &#039;fileurl&#039; or &#039;url&#039;&lt;br /&gt;
* component (string): Optional. Component the file belongs to.&lt;br /&gt;
* componentId (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* canDelete (boolean): Optional. Whether file can be deleted.&lt;br /&gt;
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they&#039;re outdated or not.&lt;br /&gt;
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-file [file]=&amp;quot;{fileurl: &#039;&amp;lt;% issue.url %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.name %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, filesize: &#039;&amp;lt;% issue.size %&amp;gt;&#039;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-file&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-download-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.&lt;br /&gt;
&lt;br /&gt;
It is usually recommended to use the core-file component since it also displays the state of the file.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;core-download-file&#039;&#039;&#039; (object): The file to download.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage: a button to download a file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button [core-download-file]=&amp;quot;{fileurl: &amp;lt;% issue.url %&amp;gt;, timemodified: &amp;lt;% issue.timemodified %&amp;gt;, filesize: &amp;lt;% issue.size %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.download | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-course-download-module-main-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and opening the main file of a module.&lt;br /&gt;
&lt;br /&gt;
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.&lt;br /&gt;
&lt;br /&gt;
This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;module&#039;&#039;&#039; (object): Optional. The module object. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;moduleId&#039;&#039;&#039; (number): Optional. The module ID. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): The course ID the module belongs to.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.&lt;br /&gt;
* &#039;&#039;&#039;files&#039;&#039;&#039; (object[]): Optional. List of files of the module. If not provided, use module.contents.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-navbar-buttons====&lt;br /&gt;
&lt;br /&gt;
Component to add buttons to the app&#039;s header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.&lt;br /&gt;
&lt;br /&gt;
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first &amp;lt;ion-buttons&amp;gt; found in the header.&lt;br /&gt;
&lt;br /&gt;
You can use the [hidden] input to hide all the inner buttons if a certain condition is met.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons end&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button icon-only (click)=&amp;quot;action()&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;ion-icon name=&amp;quot;funnel&amp;quot;&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also use this to add options to the context menu. Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons&amp;gt;&lt;br /&gt;
    &amp;lt;core-context-menu&amp;gt;&lt;br /&gt;
        &amp;lt;core-context-menu-item [priority]=&amp;quot;500&amp;quot; [content]=&amp;quot;&#039;Nice boat&#039;&amp;quot; (action)=&amp;quot;boatFunction()&amp;quot; [iconAction]=&amp;quot;&#039;boat&#039;&amp;quot;&amp;gt;&amp;lt;/core-context-menu-item&amp;gt;&lt;br /&gt;
    &amp;lt;/core-context-menu&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.&lt;br /&gt;
&lt;br /&gt;
===Specific component and directives for plugins===&lt;br /&gt;
&lt;br /&gt;
These are component and directives created specifically for supporting Moodle plugins.&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the new &#039;&#039;get_content&#039;&#039; WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to go to a new content page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to load new content in current page using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.&lt;br /&gt;
&lt;br /&gt;
If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;successMessage&#039;&#039;&#039; (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).&lt;br /&gt;
* &#039;&#039;&#039;goBackOnSuccess&#039;&#039;&#039; (boolean): Whether to go back if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;refreshOnSuccess&#039;&#039;&#039; (boolean): Whether to refresh the current view if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server without using cache, displaying default messages and refreshing on success:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage successMessage refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; goBackOnSuccess=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;certificateViewed($event)&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.certificateViewed = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t need to load some new content when done, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;jsData&#039;&#039;&#039; (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;newContentPreSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server without using cache, showing default confirm and displaying a new page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server using cache, without confirm, displaying new content in same page and using &#039;&#039;userid&#039;&#039; from &#039;&#039;otherdata&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-on-load====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.&lt;br /&gt;
&lt;br /&gt;
If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Advanced features==&lt;br /&gt;
&lt;br /&gt;
===Display the plugin only if certain conditions are met===&lt;br /&gt;
&lt;br /&gt;
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the &amp;quot;init&amp;quot; method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).&lt;br /&gt;
&lt;br /&gt;
All the init methods are called as soon as your plugin is retrieved. If you don&#039;t want your plugin to be displayed for the current user, then you should return an exception in this init method. It&#039;s recommended to include a message explaining why the plugin isn&#039;t available for the current user, this exception will be logged in the Javascript console.&lt;br /&gt;
&lt;br /&gt;
On the other hand, you might want to display a plugin only for certain courses (&#039;&#039;CoreCourseOptionsDelegate&#039;&#039;) or only if the user is viewing certain users&#039; profiles (&#039;&#039;CoreUserDelegate&#039;&#039;). This can be achieved with the init method too.&lt;br /&gt;
&lt;br /&gt;
In the init method you can return a &amp;quot;restrict&amp;quot; property with two fields in it: &#039;&#039;courses&#039;&#039; and &#039;&#039;users&#039;&#039;. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users&#039; profiles.&lt;br /&gt;
&lt;br /&gt;
===Using “otherdata”===&lt;br /&gt;
&lt;br /&gt;
The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a &#039;&#039;get_content&#039;&#039; WS call is added to a variable named CONTENT_OTHERDATA.&lt;br /&gt;
&lt;br /&gt;
The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.&lt;br /&gt;
&lt;br /&gt;
This means that, in your Javascript, you can access and use these data like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
this.CONTENT_OTHERDATA.myVar&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
And in the template you could use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
{{ CONTENT_OTHERDATA.myVar }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;myVar&#039;&#039; is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:&lt;br /&gt;
&lt;br /&gt;
array(&#039;myVar&#039; =&amp;gt; &#039;Initial value&#039;)&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.&lt;br /&gt;
&lt;br /&gt;
We will return the initial value of the input in the otherdata of our PHP method:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;myVar&#039; =&amp;gt; &#039;My initial value&#039;),&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then in the template we will use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-item text-wrap&amp;gt;&lt;br /&gt;
    &amp;lt;ion-label stacked&amp;gt;{{ &#039;plugin.mod_certificate.textlabel | translate }}&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;text&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.myVar&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [useOtherDataForWS]=&amp;quot;[&#039;myVar&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
        {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we are creating an input text and we use &#039;&#039;[(ngModel)]&#039;&#039; to use the value in &#039;&#039;myVar&#039;&#039; as the initial value and to store the changes in the same &#039;&#039;myVar&#039;&#039; variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the &#039;&#039;myVar&#039;&#039; variable. This is called 2-way data binding in Angular.&lt;br /&gt;
&lt;br /&gt;
Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the &#039;&#039;useOtherDataForWS&#039;&#039; attribute to specify which variable from &#039;&#039;otherdata&#039;&#039; we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService &#039;&#039;mod_certificate_my_webservice&#039;&#039; and will send as a param: myVar -&amp;gt; “A new value”.&lt;br /&gt;
&lt;br /&gt;
We can achieve the same result using the &#039;&#039;params&#039;&#039; attribute of the core-site-plugins-call-ws directive instead of using &#039;&#039;useOtherDataForWS&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [params]=&amp;quot;{myVar: CONTENT_OTHERDATA.myVar}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The WebService call will be exactly the same with both buttons.&lt;br /&gt;
&lt;br /&gt;
Please notice that this example could be done without using otherdata too, using the “&#039;&#039;form&#039;&#039;” input of the &#039;&#039;core-site-plugins-call-ws directive&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Running JS code after a content template has loaded===&lt;br /&gt;
&lt;br /&gt;
When you return JavaScript code from a handler function using the &#039;javascript&#039; array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. &lt;br /&gt;
&lt;br /&gt;
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;template&#039; =&amp;gt; [ ... ],&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;setTimeout(function() { console.log(&amp;quot;DOM is available now&amp;quot;); });&#039;,&lt;br /&gt;
    &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
    &#039;files&#039; =&amp;gt; []&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
===JS functions visible in the templates===&lt;br /&gt;
&lt;br /&gt;
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;openContent(title: string, args: any, component?: string, method?: string)&#039;&#039;&#039;: Open a new page to display some new content. You need to specify the &#039;&#039;title&#039;&#039; of the new page and the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
* &#039;&#039;&#039;refreshContent(showSpinner = true)&#039;&#039;&#039;: Refresh the current content. By default it will display a spinner while refreshing, if you don&#039;t want it to be displayed you should pass false as a parameter.&lt;br /&gt;
* &#039;&#039;&#039;updateContent(args: any, component?: string, method?: string)&#039;&#039;&#039;: Refresh the current content using different params. You need to specify the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Group selector=====&lt;br /&gt;
&lt;br /&gt;
Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.&lt;br /&gt;
&lt;br /&gt;
To do so, we&#039;ll use a drop down to select the group. When the user selects a group using this drop down we&#039;ll update the page content to display the new group.&lt;br /&gt;
&lt;br /&gt;
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular&#039;s &#039;&#039;ngModel&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
======Using plain HTML======&lt;br /&gt;
&lt;br /&gt;
We need to add a &amp;quot;&#039;&#039;selected&#039;&#039;&amp;quot; attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
        // Detect which group is selected.&lt;br /&gt;
        foreach ($groups as $gid=&amp;gt;$group) {&lt;br /&gt;
            $group-&amp;gt;selected = $gid === $groupid;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid,&lt;br /&gt;
            &#039;groups&#039; =&amp;gt; $groups&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the code above, we&#039;re retrieving the groups the user can see and then we&#039;re adding a &amp;quot;selected&amp;quot; bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.&lt;br /&gt;
&lt;br /&gt;
In the template, we display the drop down like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: $event})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot; &amp;lt;%#selected%&amp;gt;selected&amp;lt;%/selected%&amp;gt; &amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;ionChange&#039;&#039; function will be called everytime the user selects a different group with the drop down. We&#039;re using the function &#039;&#039;updateContent&#039;&#039; to update the current view using the new group. &#039;&#039;$event&#039;&#039; is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.&lt;br /&gt;
&lt;br /&gt;
======Using ngModel======&lt;br /&gt;
&lt;br /&gt;
ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn&#039;t have &#039;&#039;ng-init&#039;&#039; like in AngularJS), so we&#039;ll use &amp;quot;otherdata&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
In the PHP function we&#039;ll return the group that needs to be selected in the &#039;&#039;otherdata&#039;&#039; array:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
         return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; array(&lt;br /&gt;
                &#039;group&#039; =&amp;gt; $groupid&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we don&#039;t need to iterate over the groups array like in the plain HTML example. However, now we&#039;re returning the groupid in the &amp;quot;otherdata&amp;quot; array. As it&#039;s explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using &amp;quot;otherdata&amp;quot;]] section, this &amp;quot;otherdata&amp;quot; is visible in the templates inside a variable named &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. So in the template we&#039;ll use this variable like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.group&amp;quot; (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: CONTENT_OTHERDATA.group})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Use the rich text editor===&lt;br /&gt;
&lt;br /&gt;
The rich text editor included in the app requires a FormControl to work. You can use the library FormBuilder to create this control (or to create a whole FormGroup if you prefer).&lt;br /&gt;
&lt;br /&gt;
With the following Javascript you&#039;ll be able to create a FormControl:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we&#039;re using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.&lt;br /&gt;
&lt;br /&gt;
Then you need to pass this control to the rich text editor in your template:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;core-rich-text-editor item-content [control]=&amp;quot;control&amp;quot; placeholder=&amp;quot;Enter your text here&amp;quot; name=&amp;quot;rte_answer&amp;quot;&amp;gt;&amp;lt;/core-rich-text-editor&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, there are several ways to send the value in the rich text editor to a WebService to save it. This is one of the simplest options:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_webservice&amp;quot; [params]=&amp;quot;{rte: control.value}&amp;quot; ....&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re passing the value of the rich text editor as a parameter to our WebService.&lt;br /&gt;
&lt;br /&gt;
===Initialization===&lt;br /&gt;
&lt;br /&gt;
All handlers can specify a “&#039;&#039;init&#039;&#039;” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.&lt;br /&gt;
&lt;br /&gt;
When the app retrieves all the handlers, the first thing it will do is call the &#039;&#039;tool_mobile_get_content&#039;&#039; WebService with the init method. This WS call will only receive the default args.&lt;br /&gt;
&lt;br /&gt;
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.&lt;br /&gt;
&lt;br /&gt;
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.INIT_TEMPLATES[‘main’];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
In this case, “main” is the ID of the template we want to use.&lt;br /&gt;
&lt;br /&gt;
The same happens with the &#039;&#039;otherdata&#039;&#039; returned by this init method, it is added to a INIT_OTHERDATA variable.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;restrict&#039;&#039; field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate &#039;&#039;CoreCourseOptionsDelegate&#039;&#039; and you return a list of courseids in restrict-&amp;gt;courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.&lt;br /&gt;
&lt;br /&gt;
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    MyAddonClass: new MyAddonClass()&lt;br /&gt;
};&lt;br /&gt;
result:&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.MyAddonClass&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Module link handler=====&lt;br /&gt;
&lt;br /&gt;
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function AddonModCertificateModuleLinkHandler() {&lt;br /&gt;
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, &#039;mmaModCertificate&#039;, &#039;certificate&#039;);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateLinkHandler&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;&lt;br /&gt;
&lt;br /&gt;
this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Advanced link handler=====&lt;br /&gt;
Link handlers have some advanced features that allow you to change how links behave under different conditions.&lt;br /&gt;
======Patterns======&lt;br /&gt;
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.pattern = RegExp(&#039;\/mod\/foo\/specialpage.php&#039;);&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Priority======&lt;br /&gt;
Multiple link handlers may apply to a given link.  You can define the order of precedence by setting the priority - the handler with the highest priority will be used.&lt;br /&gt;
All default handlers have a priority of 0, so 1 or higher will override the default.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Multiple actions======&lt;br /&gt;
Once a link has been matched, the handler&#039;s getActions() method determines what the link should do.  This method has access to the URL and its parameters.&lt;br /&gt;
Different actions can be returned depending on different conditions.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {&lt;br /&gt;
    return [{&lt;br /&gt;
        action: function(siteId, navCtrl) {&lt;br /&gt;
            // The actual behaviour of the link goes here.&lt;br /&gt;
        },&lt;br /&gt;
        sites: [...]&lt;br /&gt;
    }, {&lt;br /&gt;
        ...&lt;br /&gt;
    }];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order.  The first &amp;quot;valid&amp;quot; action will be used to open the link.&lt;br /&gt;
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can &amp;quot;invalidate&amp;quot;&lt;br /&gt;
your action by settings its sites propety to an empty array.&lt;br /&gt;
======Complex example======&lt;br /&gt;
This will match all URLs containing /mod/foo/, and force those with an id parameter that&#039;s not in the &amp;quot;supportedModFoos&amp;quot; array to open in the user&#039;s browser, rather than the app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
var supportedModFoos = [...];&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    this.pattern = new RegExp(&#039;\/mod\/foo\/&#039;);&lt;br /&gt;
    this.name = &amp;quot;AddonModFooLinkHandler&amp;quot;;&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
}&lt;br /&gt;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);&lt;br /&gt;
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     &lt;br /&gt;
    var action = {&lt;br /&gt;
        action: function() {&lt;br /&gt;
            that.CoreUtilsProvider.openInBrowser(url);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {&lt;br /&gt;
        action.sites = [];&lt;br /&gt;
    }&lt;br /&gt;
    return [action];&lt;br /&gt;
};&lt;br /&gt;
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Module prefetch handler=====&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;CoreCourseModuleDelegate&#039;&#039; handler allows you to define a list of &#039;&#039;offlinefunctions&#039;&#039; to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using &#039;&#039;offlinefunctions&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to create a prefetch handler using init JS:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
// Create a class that &amp;quot;inherits&amp;quot; from CoreCourseActivityPrefetchHandlerBase.&lt;br /&gt;
function AddonModCertificateModulePrefetchHandler() {&lt;br /&gt;
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,&lt;br /&gt;
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateModulePrefetchHandler&amp;quot;;&lt;br /&gt;
    this.modName = &amp;quot;certificate&amp;quot;;&lt;br /&gt;
    this.component = &amp;quot;mod_certificate&amp;quot;; // This must match the plugin identifier from db/mobile.php, otherwise the download link in the context menu will not update correctly.&lt;br /&gt;
    this.updatesNames = /^configuration$|^.*files$/;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;&lt;br /&gt;
&lt;br /&gt;
// Override the prefetch call.&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {&lt;br /&gt;
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function prefetchCertificate(module, courseId, single, siteId) {&lt;br /&gt;
    // Perform all the WS calls.&lt;br /&gt;
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won&#039;t be able to prefetch all the possible parameters. &lt;br /&gt;
&lt;br /&gt;
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom &#039;section&#039; parameter for the mobile function &#039;mobile_document_view&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function prefetchOucontent(module, courseId, single, siteId) {&lt;br /&gt;
    var component = &#039;mod_oucontent&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Get the site, first.&lt;br /&gt;
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {&lt;br /&gt;
        // Read the list of pages in this document using a web service.&lt;br /&gt;
        return site.read(&#039;mod_oucontent_get_page_list&#039;, {&#039;cmid&#039;: module.id}).then(function(response) {&lt;br /&gt;
            var promises = [];&lt;br /&gt;
&lt;br /&gt;
            // For each page, read and process the page - this is a copy of logic in the app at&lt;br /&gt;
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.&lt;br /&gt;
            for(var i = 0; i &amp;lt; response.length; i++) {&lt;br /&gt;
                var args = {&lt;br /&gt;
                    courseid: courseId,&lt;br /&gt;
                    cmid: module.id,&lt;br /&gt;
                    userid: site.getUserId()&lt;br /&gt;
                };&lt;br /&gt;
                if (response[i] !== &#039;&#039;) {&lt;br /&gt;
                    args.section = response[i];&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                promises.push(that.CoreSitePluginsProvider.getContent(&lt;br /&gt;
                        component, &#039;mobile_document_view&#039;, args).then(&lt;br /&gt;
                        function(result) {&lt;br /&gt;
                            var subPromises = [];&lt;br /&gt;
                            if (result.files &amp;amp;&amp;amp; result.files.length) {&lt;br /&gt;
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(&lt;br /&gt;
                                        site.id, result.files, true, false, component, module.id));&lt;br /&gt;
                            }&lt;br /&gt;
                            return Promise.all(subPromises);&lt;br /&gt;
                        }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            return Promise.all(promises);&lt;br /&gt;
        });&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Single activity course format=====&lt;br /&gt;
&lt;br /&gt;
In the following example, the value of INIT_TEMPLATES[&amp;quot;main&amp;quot;] is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;core-dynamic-component [component]=&amp;quot;componentClass&amp;quot; [data]=&amp;quot;data&amp;quot;&amp;gt;&amp;lt;/core-dynamic-component&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This template is returned by the init method. And this is the JavaScript code returned by the init method:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function getAddonSingleActivityFormatComponent() {&lt;br /&gt;
    function AddonSingleActivityFormatComponent() {&lt;br /&gt;
        this.data = {};&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {&lt;br /&gt;
        var self = this;&lt;br /&gt;
&lt;br /&gt;
        if (this.course &amp;amp;&amp;amp; this.sections &amp;amp;&amp;amp; this.sections.length) {&lt;br /&gt;
            var module = this.sections[0] &amp;amp;&amp;amp; this.sections[0].modules &amp;amp;&amp;amp; this.sections[0].modules[0];&lt;br /&gt;
            if (module &amp;amp;&amp;amp; !this.componentClass) {&lt;br /&gt;
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) =&amp;gt; {&lt;br /&gt;
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.data.courseId = this.course.id;&lt;br /&gt;
            this.data.module = module;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {&lt;br /&gt;
        return Promise.resolve(this.dynamicComponent.callComponentFunction(&amp;quot;doRefresh&amp;quot;, [refresher, done]));&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
return AddonSingleActivityFormatComponent;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function AddonSingleActivityFormatHandler() {&lt;br /&gt;
    this.name = &amp;quot;singleactivity&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.isEnabled = function() {&lt;br /&gt;
    return true;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {&lt;br /&gt;
    if (sections &amp;amp;&amp;amp; sections[0] &amp;amp;&amp;amp; sections[0].modules &amp;amp;&amp;amp; sections[0].modules[0]) {&lt;br /&gt;
        return sections[0].modules[0].name;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return course.fullname || &amp;quot;&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {&lt;br /&gt;
    that.Injector = injector || that.Injector;&lt;br /&gt;
&lt;br /&gt;
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES[&amp;quot;main&amp;quot;], getAddonSingleActivityFormatComponent(), injector);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using the JavaScript API===&lt;br /&gt;
&lt;br /&gt;
The Javascript API is partly supported right now, only the delegates specified in the section [[Mobile_support_for_plugins#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] supports it now. This API allows you to override any of the functions of the default handler. &lt;br /&gt;
&lt;br /&gt;
The “method” specified in a handler registered in the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.&lt;br /&gt;
&lt;br /&gt;
For example, if the Javascript returned by the method returns something like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The the &#039;&#039;getData&#039;&#039; function of the default handler will be overridden by the returned getData function.&lt;br /&gt;
&lt;br /&gt;
The default handler for &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; only has 2 functions: &#039;&#039;getComponent&#039;&#039; and &#039;&#039;getData&#039;&#039;. In addition, the JavaScript code can return an extra function named &#039;&#039;componentInit&#039;&#039; that will be executed when the component returned by &#039;&#039;getComponent&#039;&#039; is initialized.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to support the text user profile field using this API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
var result = {&lt;br /&gt;
    componentInit: function() {&lt;br /&gt;
        if (this.field &amp;amp;&amp;amp; this.edit &amp;amp;&amp;amp; this.form) {&lt;br /&gt;
            this.field.modelName = &amp;quot;profile_field_&amp;quot; + this.field.shortname;&lt;br /&gt;
&lt;br /&gt;
            if (this.field.param2) {&lt;br /&gt;
                this.field.maxlength = parseInt(this.field.param2, 10) || &amp;quot;&amp;quot;;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? &amp;quot;password&amp;quot; : &amp;quot;text&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var formData = {&lt;br /&gt;
                value: this.field.defaultdata,&lt;br /&gt;
                disabled: this.disabled&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required &amp;amp;&amp;amp; !this.field.locked ? that.Validators.required : null));&lt;br /&gt;
        }&lt;br /&gt;
    },&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
        var name = &amp;quot;profile_field_&amp;quot; + field.shortname;&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            type: &amp;quot;text&amp;quot;,&lt;br /&gt;
            name: name,&lt;br /&gt;
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name])&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Translate dynamic strings===&lt;br /&gt;
&lt;br /&gt;
If you wish to have an element that displays a localised string based on value from your template you can doing something like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content translate&amp;gt;&lt;br /&gt;
        plugin.mod_myactivity.&amp;lt;% status %&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This could save you from having to write something like when only one value should be displayed:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content&amp;gt;&lt;br /&gt;
        &amp;lt;%#isedting%&amp;gt;{{ &#039;plugin.mod_myactivity.editing&#039; | translate }}&amp;lt;%/isediting%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isopen%&amp;gt;{{ &#039;plugin.mod_myactivity.open&#039; | translate }}&amp;lt;%/isopen%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isclosed%&amp;gt;{{ &#039;plugin.mod_myactivity.closed&#039; | translate }}&amp;lt;%/isclosed%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using strings with dates===&lt;br /&gt;
&lt;br /&gt;
If you have a string that you wish to pass a formatted date for example in the Moodle language file you have:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;strwithdate&#039;] = &#039;This string includes a date of {$a-&amp;gt;date} in the middle of it.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can localise the string correctly in your template using something like the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{{ &#039;plugin.mod_myactivity.strwithdate&#039; | translate: {$a: { date: &amp;lt;% timestamp %&amp;gt; * 1000 | coreFormatDate: &amp;quot;dffulldate&amp;quot; } } }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, where as Unix timestamps are in seconds.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
=== Invalid response received ===&lt;br /&gt;
&lt;br /&gt;
You might receive this error when using the &amp;quot;core-site-plugins-call-ws&amp;quot; directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{typeExpected: &#039;boolean&#039;}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{responseExpected: false}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS ===&lt;br /&gt;
&lt;br /&gt;
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don&#039;t use HTML inputs, they simulate them, so the directive isn&#039;t going to find their data and so it won&#039;t be sent to the WebService.&lt;br /&gt;
&lt;br /&gt;
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.&lt;br /&gt;
&lt;br /&gt;
==== Sending the data manually ====&lt;br /&gt;
&lt;br /&gt;
The first solution is to send the missing params manually using the &amp;quot;&#039;&#039;params&#039;&#039;&amp;quot; property. We will use &#039;&#039;ngModel&#039;&#039; to store the input value in a variable, and this variable will be passed to the params. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a template like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group name=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, responses: responses}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Basically, you need to add &#039;&#039;ngModel&#039;&#039; to the affected element (in this case, the &#039;&#039;radio-group&#039;&#039;). You can put whatever name you want as the value, we used &amp;quot;responses&amp;quot;. With this, everytime the user selects a radio button the value will be stored in a variable named &amp;quot;responses&amp;quot;. Then, in the button we are passing this variable to the params of the WebService.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &amp;quot;form&amp;quot; attribute has priority over &amp;quot;params&amp;quot;, so if you have an input with name=&amp;quot;responses&amp;quot; it will override what you&#039;re manually passing to params.&lt;br /&gt;
&lt;br /&gt;
==== Using a hidden input ====&lt;br /&gt;
&lt;br /&gt;
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use &#039;&#039;ngModel&#039;&#039; to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a radio button like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot; [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;hidden&amp;quot; [ngModel]=&amp;quot;responses&amp;quot; name=&amp;quot;responses&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we&#039;re using a variable named &amp;quot;responses&amp;quot; to synchronize the data between the &#039;&#039;radio-group&#039;&#039; and the hidden input. You can use whatever name you want.&lt;br /&gt;
&lt;br /&gt;
=== I can&#039;t return an object or array in otherdata ===&lt;br /&gt;
&lt;br /&gt;
If you try to return an object or an array in any field inside &#039;&#039;otherdata&#039;&#039;, the WebService call will fail with the following error:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Scalar type expected, array or object received&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Each field in &#039;&#039;otherdata&#039;&#039; must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($data))&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The app will automatically parse this JSON and convert it back into an array or object.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===Accepting dynamic names in a WebService===&lt;br /&gt;
&lt;br /&gt;
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.&lt;br /&gt;
&lt;br /&gt;
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they&#039;re dynamic. The solution is to accept an array of objects with name and value. So in the &#039;&#039;_parameters()&#039;&#039; function of our new WebService, we will add this parameter:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;data&#039; =&amp;gt; new external_multiple_structure(&lt;br /&gt;
     new external_single_structure(&lt;br /&gt;
        array(&lt;br /&gt;
            &#039;name&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data name&#039;),&lt;br /&gt;
            &#039;value&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data value&#039;),&lt;br /&gt;
        )&lt;br /&gt;
    ),&lt;br /&gt;
    &#039;The data to be saved&#039;, VALUE_DEFAULT, array()&lt;br /&gt;
)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive &#039;&#039;core-site-plugins-call-ws&#039;&#039; that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the &amp;quot;&#039;&#039;form&#039;&#039;&amp;quot; attribute, because we need to format the data before it is sent.&lt;br /&gt;
&lt;br /&gt;
Since we will send the params manually and we want it all to be sent in the same array, we will use &#039;&#039;ngModel&#039;&#039; to store the input data into a variable that we&#039;ll call &amp;quot;data&amp;quot;, but you can use the name you want. This &amp;quot;data&amp;quot; will be an object that will hold the input data with the format &amp;quot;name-&amp;gt;value&amp;quot;. For example, if I have an input with name &amp;quot;a1&amp;quot; and value &amp;quot;My answer&amp;quot;, the data object will be:&lt;br /&gt;
&lt;br /&gt;
{a1: &amp;quot;My answer&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
So we need to add &#039;&#039;ngModel&#039;&#039; to all the inputs whose values need to be sent to the &amp;quot;data&amp;quot; WS param. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&amp;lt;ion-input name=&amp;quot;&amp;lt;% name %&amp;gt;&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.data[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re using &#039;&#039;CONTENT_OTHERDATA&#039;&#039; to store the data. We do it like this because we&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the form, setting the values the user has already stored. If you don&#039;t need to initialize the form, then you can use the variable &amp;quot;dataObject&amp;quot;, an empty object that the Mobile app creates for you: [(ngModel)]=&amp;quot;dataObject[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: &#039;&#039;objectToArrayOfObjects&#039;&#039;. So in our button we&#039;ll use this function to format the data before it&#039;s sent:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_ws_name&amp;quot;&lt;br /&gt;
    [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, &#039;name&#039;, &#039;value&#039;)}&amp;quot;&lt;br /&gt;
    successMessage&lt;br /&gt;
    refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see in the example above, we&#039;re specifying that the keys of the &amp;quot;data&amp;quot; object need to be stored in a property named &amp;quot;name&amp;quot;, and the values need to be stored in a property named &amp;quot;value&amp;quot;. If your WebService expects different names you need to change the parameters of the function &#039;&#039;objectToArrayOfObjects&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable &amp;quot;data&amp;quot; doesn&#039;t exist inside &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. As it is explained in previous sections, &#039;&#039;CONTENT_OTHERDATA&#039;&#039; holds the data that you return in &#039;&#039;otherdata&#039;&#039; for your method. We&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the values to be displayed in the form.&lt;br /&gt;
&lt;br /&gt;
If the user hasn&#039;t answered the form yet, we can initialize the &amp;quot;data&amp;quot; object as an empty object. Please remember that we cannot return arrays or objects in &#039;&#039;otherdata&#039;&#039;, so we&#039;ll return a JSON string.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; &#039;{}&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$userdata = get_user_responses(); // It will held the data in a format name-&amp;gt;value. Example: array(&#039;a1&#039; =&amp;gt; &#039;My value&#039;).&lt;br /&gt;
...&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($userdata))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.&lt;br /&gt;
&lt;br /&gt;
==Moodle plugins with mobile support==&lt;br /&gt;
&lt;br /&gt;
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].&lt;br /&gt;
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].&lt;br /&gt;
* Gapfill question type: [https://moodle.org/plugins/qtype_gapfill Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_gapfill in github].&lt;br /&gt;
* Wordselect question type: [https://moodle.org/plugins/qtype_wordselect Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_wordselect in github].&lt;br /&gt;
* RegExp question type: [https://moodle.org/plugins/qtype_regexp Moodle plugins directory entry] and [https://github.com/rezeau/moodle-qtype_regexp in github].&lt;br /&gt;
* Certificate: [https://moodle.org/plugins/mod_certificate Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_certificate in github].&lt;br /&gt;
* Attendance [https://moodle.org/plugins/mod_attendance Moodle plugins directory entry] and [https://github.com/danmarsden/moodle-mod_attendance in github].&lt;br /&gt;
&lt;br /&gt;
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&amp;amp;id=6 here] (it may contain some outdated plugins)&lt;br /&gt;
[[Category:Mobile]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55754</id>
		<title>Moodle App Plugins Development Guide</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55754"/>
		<updated>2019-03-18T13:51:47Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Advanced features */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
==Before 3.5==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle Mobile Remote add-ons|Remote add-ons]] for details.&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5 the app switched to a new way to suport plugins that was much easier for developers.&lt;br /&gt;
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).&lt;br /&gt;
* The use of JavaScript is optional (but some type of advanced plugins may require it)&lt;br /&gt;
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).&lt;br /&gt;
&lt;br /&gt;
This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)&lt;br /&gt;
&lt;br /&gt;
This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.&lt;br /&gt;
&lt;br /&gt;
==How it works==&lt;br /&gt;
&lt;br /&gt;
The overall idea is to allow Moodle plugins to extend different areas in the app with &#039;&#039;just PHP server side&#039;&#039; code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.&lt;br /&gt;
&lt;br /&gt;
Developers will have to:&lt;br /&gt;
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.&lt;br /&gt;
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates. &lt;br /&gt;
&lt;br /&gt;
Let’s clarify some points:&lt;br /&gt;
&lt;br /&gt;
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.&lt;br /&gt;
* Those functions will be exported via the Web Service function tool_mobile_get_content&lt;br /&gt;
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).&lt;br /&gt;
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..&lt;br /&gt;
&lt;br /&gt;
==Types of plugins==&lt;br /&gt;
&lt;br /&gt;
We could classify all the plugins in 3 different types:&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_when_requested.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you&#039;re developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_on_login.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
==Step by step example==&lt;br /&gt;
&lt;br /&gt;
In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.&lt;br /&gt;
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.&lt;br /&gt;
&lt;br /&gt;
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)&lt;br /&gt;
&lt;br /&gt;
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.&lt;br /&gt;
&lt;br /&gt;
===Step 1. Update the db/mobile.php file===&lt;br /&gt;
In this case, we are updating an existing file but for new plugins, you should create this new file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$addons = [&lt;br /&gt;
    &#039;mod_certificate&#039; =&amp;gt; [ // Plugin identifier&lt;br /&gt;
        &#039;handlers&#039; =&amp;gt; [ // Different places where the plugin will display content.&lt;br /&gt;
            &#039;coursecertificate&#039; =&amp;gt; [ // Handler unique name (alphanumeric).&lt;br /&gt;
                &#039;displaydata&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;icon&#039; =&amp;gt; $CFG-&amp;gt;wwwroot . &#039;/mod/certificate/pix/icon.gif&#039;,&lt;br /&gt;
                    &#039;class&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
                ],&lt;br /&gt;
       &lt;br /&gt;
                &#039;delegate&#039; =&amp;gt; &#039;CoreCourseModuleDelegate&#039;, // Delegate (where to display the link to the plugin)&lt;br /&gt;
                &#039;method&#039; =&amp;gt; &#039;mobile_course_view&#039;, // Main function in \mod_certificate\output\mobile&lt;br /&gt;
                &#039;offlinefunctions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;mobile_course_view&#039; =&amp;gt; [],&lt;br /&gt;
                    &#039;mobile_issues_view&#039; =&amp;gt; [],&lt;br /&gt;
                ]. // Function that needs to be downloaded for offline.&lt;br /&gt;
            ],&lt;br /&gt;
        ],&lt;br /&gt;
        &#039;lang&#039; =&amp;gt; [ // Language strings that are used in all the handlers.&lt;br /&gt;
            [&#039;pluginname&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;summaryofattempts&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;getcertificate&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;requiredtimenotmet&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;viewcertificateviews&#039;, &#039;certificate&#039;],&lt;br /&gt;
        ],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Plugin identifier:&lt;br /&gt;
: A unique name for the plugin, it can be anything (there’s no need to match the module name).&lt;br /&gt;
 &lt;br /&gt;
;Handlers  (Different places where the plugin will display content):&lt;br /&gt;
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).&lt;br /&gt;
&lt;br /&gt;
; Display data:&lt;br /&gt;
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.&lt;br /&gt;
	&lt;br /&gt;
; Delegate&lt;br /&gt;
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.&lt;br /&gt;
&lt;br /&gt;
; Method:&lt;br /&gt;
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.	&lt;br /&gt;
&lt;br /&gt;
; Offlinefunctions&lt;br /&gt;
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file. &lt;br /&gt;
: In our example, downloading for offline access will mean that we&#039;ll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we&#039;ll be able to display the certificate information even if the user is offline.&lt;br /&gt;
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).&lt;br /&gt;
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)&lt;br /&gt;
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the &#039;&#039;files&#039;&#039; array).&lt;br /&gt;
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module&#039;s view function by using a &#039;page&#039; parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].&lt;br /&gt;
&lt;br /&gt;
;Lang:&lt;br /&gt;
: The language pack string ids used in the plugin by all the handlers. Please note that you should avoid adding all the plugin string ids (including those unused) because the Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform.&lt;br /&gt;
&lt;br /&gt;
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.&lt;br /&gt;
&lt;br /&gt;
===Step 2. Creating the main function===&lt;br /&gt;
&lt;br /&gt;
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.&lt;br /&gt;
&lt;br /&gt;
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.&lt;br /&gt;
&lt;br /&gt;
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
namespace mod_certificate\output;&lt;br /&gt;
&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
&lt;br /&gt;
use context_module;&lt;br /&gt;
use mod_certificate_external;&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Mobile output class for certificate&lt;br /&gt;
 *&lt;br /&gt;
 * @package    mod_certificate&lt;br /&gt;
 * @copyright  2018 Juan Leyva&lt;br /&gt;
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later&lt;br /&gt;
 */&lt;br /&gt;
class mobile {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate course view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_course_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Set timemodified for each certificate.&lt;br /&gt;
        foreach ($issues as $issue) {&lt;br /&gt;
            if (empty($issue-&amp;gt;timemodified)) {&lt;br /&gt;
                    $issue-&amp;gt;timemodified = $issue-&amp;gt;timecreated;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $showget = true;&lt;br /&gt;
        if ($certificate-&amp;gt;requiredtime &amp;amp;&amp;amp; !has_capability(&#039;mod/certificate:manage&#039;, $context)) {&lt;br /&gt;
            if (certificate_get_course_time($certificate-&amp;gt;course) &amp;lt; ($certificate-&amp;gt;requiredtime * 60)) {&lt;br /&gt;
                    $showget = false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $certificate-&amp;gt;name = format_string($certificate-&amp;gt;name);&lt;br /&gt;
        list($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat) =&lt;br /&gt;
                        external_format_text($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat, $context-&amp;gt;id,&#039;mod_certificate&#039;, &#039;intro&#039;);&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;certificate&#039; =&amp;gt; $certificate,&lt;br /&gt;
            &#039;showget&#039; =&amp;gt; $showget &amp;amp;&amp;amp; count($issues) &amp;gt; 0,&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues,&lt;br /&gt;
            &#039;issue&#039; =&amp;gt; $issues[0],&lt;br /&gt;
            &#039;numissues&#039; =&amp;gt; count($issues),&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;files&#039; =&amp;gt; $issues,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let’s go through the function code to analyse the different parts.&lt;br /&gt;
&lt;br /&gt;
;Function declaration: &lt;br /&gt;
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument “$args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)&lt;br /&gt;
&lt;br /&gt;
; Function implementation:&lt;br /&gt;
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.&lt;br /&gt;
&lt;br /&gt;
Finally, we return:&lt;br /&gt;
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.&lt;br /&gt;
* JavaScript: Empty, because we don’t need any in this case&lt;br /&gt;
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.&lt;br /&gt;
* Files: A list of files that the app should be able to download (for offline usage mostly)&lt;br /&gt;
&lt;br /&gt;
===Step 3. Creating the template for the main function===&lt;br /&gt;
&lt;br /&gt;
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.&lt;br /&gt;
&lt;br /&gt;
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.&lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/ &lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with &#039;&#039;core-&#039;&#039; are custom components of the Mobile app.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_page.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;core-course-module-description description=&amp;quot;&amp;lt;% certificate.intro %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-course-module-description&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;ion-list-header&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-list-header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
                    {{ &#039;plugin.mod_certificate.viewcertificateviews&#039; | translate: {$a: &amp;lt;% numissues %&amp;gt;} }}&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;ion-icon name=&amp;quot;cloud-download&amp;quot; item-start&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
                {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%^showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;p&amp;gt;{{ &#039;plugin.mod_certificate.requiredtimenotmet&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;!-- Call log WS when the template is loaded. --&amp;gt;&lt;br /&gt;
        &amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Then we display the module description using &amp;lt;code&amp;gt;&amp;lt;core-course-module-description&amp;lt;/code&amp;gt; that is a component used to include the course module description.&lt;br /&gt;
&lt;br /&gt;
For displaying the certificate information we create a list of elements, adding a header on top.&lt;br /&gt;
The following line &amp;lt;code&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/code&amp;gt; indicates that the Mobile app will translate the &#039;&#039;summaryofattempts&#039;&#039; string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format: &lt;br /&gt;
&lt;br /&gt;
“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php). &lt;br /&gt;
&lt;br /&gt;
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) &amp;lt;code&amp;gt;core-site-plugins-new-content&amp;lt;/code&amp;gt; indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).&lt;br /&gt;
&lt;br /&gt;
Just after this button we display another one but this time for downloading an issued certificate. The &amp;lt;code&amp;gt;core-course-download-module-main-file&amp;lt;/code&amp;gt; directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.&lt;br /&gt;
&lt;br /&gt;
Finally, just before the ion-list is closed, we use the &amp;lt;code&amp;gt;core-site-plugins-call-ws-on-load&amp;lt;/code&amp;gt; directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the &#039;&#039;mod_certificate_view_certificate&#039;&#039; that will log that the user viewed this page.&lt;br /&gt;
&lt;br /&gt;
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.&lt;br /&gt;
&lt;br /&gt;
===Step 4. Adding an additional page===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Partial file contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate issues view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_issues_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = [&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_issues&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.&lt;br /&gt;
&lt;br /&gt;
The code of the mustache template is also very simple:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_issues.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &amp;lt;%timecreated%&amp;gt; | coreToLocaleString }}&amp;lt;/p&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;&amp;lt;%grade%&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Here we are creating an ionic list that will display a new item in the list per each issued certificated.&lt;br /&gt;
&lt;br /&gt;
For the issued certificated we’ll display the time when it was created (using the app filter &#039;&#039;coreToLocaleString&#039;&#039;). We are also displaying the grade displayed in the certificate (if any).&lt;br /&gt;
&lt;br /&gt;
===Step 5. Plugin webservices, if included===&lt;br /&gt;
&lt;br /&gt;
If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.&lt;br /&gt;
&lt;br /&gt;
The following line &amp;lt;code&amp;gt;&#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&amp;lt;/code&amp;gt; should be included in each webservice definition.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/db/services.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$functions = [&lt;br /&gt;
&lt;br /&gt;
    &#039;mod_certificate_get_certificates_by_courses&#039; =&amp;gt; [&lt;br /&gt;
        &#039;classname&#039;     =&amp;gt; &#039;mod_certificate_external&#039;,&lt;br /&gt;
        &#039;methodname&#039;    =&amp;gt; &#039;get_certificates_by_courses&#039;,&lt;br /&gt;
        &#039;description&#039;   =&amp;gt; &#039;Returns a list of certificate instances...&#039;,&lt;br /&gt;
        &#039;type&#039;          =&amp;gt; &#039;read&#039;,&lt;br /&gt;
        &#039;capabilities&#039;  =&amp;gt; &#039;mod/certificate:view&#039;,&lt;br /&gt;
        &#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
	&lt;br /&gt;
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.&lt;br /&gt;
&lt;br /&gt;
==Getting started==&lt;br /&gt;
&lt;br /&gt;
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!&lt;br /&gt;
&lt;br /&gt;
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app.&lt;br /&gt;
&lt;br /&gt;
Please test that your site works correctly in the web version before starting any development.&lt;br /&gt;
&lt;br /&gt;
===Moodle version requirements===&lt;br /&gt;
&lt;br /&gt;
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin]. &lt;br /&gt;
&lt;br /&gt;
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).&lt;br /&gt;
&lt;br /&gt;
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.&lt;br /&gt;
&lt;br /&gt;
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.&lt;br /&gt;
&lt;br /&gt;
===Development workflow===&lt;br /&gt;
&lt;br /&gt;
First of all, we recommend creating a simple &#039;&#039;mobile.php&#039;&#039; for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Check that you can correctly  see the new menu option you included.&lt;br /&gt;
&lt;br /&gt;
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.&lt;br /&gt;
&lt;br /&gt;
It is important to remember that:&lt;br /&gt;
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).&lt;br /&gt;
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).&lt;br /&gt;
&lt;br /&gt;
===Testing and debugging===&lt;br /&gt;
&lt;br /&gt;
To learn how to debug with the web version of the app, please read the following documents:&lt;br /&gt;
* [[Moodle Mobile debugging WS requests]] AND&lt;br /&gt;
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)&lt;br /&gt;
&lt;br /&gt;
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.&lt;br /&gt;
&lt;br /&gt;
Within the app, make sure to turn on the option: &#039;&#039;&#039;App settings&#039;&#039;&#039; / &#039;&#039;&#039;General&#039;&#039;&#039; / &#039;&#039;&#039;Display debug messages&#039;&#039;&#039;. This means popup errors from the app will show more information.&lt;br /&gt;
&lt;br /&gt;
==Mobile.php supported options==&lt;br /&gt;
&lt;br /&gt;
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:&lt;br /&gt;
&lt;br /&gt;
===Common options===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;delegate&#039;&#039;&#039; (mandatory): Name of the delegate to register the handler in.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (mandatory): The function to call to retrieve the main page content.&lt;br /&gt;
* &#039;&#039;&#039;init&#039;&#039;&#039; (optional): A function to call to retrieve the initialization JS and the &amp;quot;restrict&amp;quot; to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.&lt;br /&gt;
* &#039;&#039;&#039;restricttocurrentuser&#039;&#039;&#039; (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;restricttoenrolledcourses&#039;&#039;&#039; (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;styles&#039;&#039;&#039; (optional): An array with two properties: &#039;&#039;url&#039;&#039; and &#039;&#039;version&#039;&#039;. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It&#039;s recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseOptionsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreMainMenuDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the &amp;quot;More&amp;quot; tab, they cannot be displayed as tabs in the bottom bar.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseModuleDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): icon, class.&lt;br /&gt;
* &#039;&#039;&#039;offlinefunctions&#039;&#039;&#039;: (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + &#039;id&#039; (e.g. certificateid).&lt;br /&gt;
* &#039;&#039;&#039;downloadbutton&#039;&#039;&#039;: (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.&lt;br /&gt;
* &#039;&#039;&#039;isresource&#039;&#039;&#039;: (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the &amp;quot;contents&amp;quot; field, then it should be true.&lt;br /&gt;
* &#039;&#039;&#039;updatesnames&#039;&#039;&#039;: (optional) Only used if there is any offlinefunction. A Regular Expression to check if there&#039;s any update in the module. It will be compared to the result of &#039;&#039;core_course_check_updates&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;displayopeninbrowser&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Open in browser&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;&lt;br /&gt;
* &#039;&#039;&#039;displaydescription&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Description&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;&lt;br /&gt;
* &#039;&#039;&#039;displayrefresh&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Refresh&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;&lt;br /&gt;
* &#039;&#039;&#039;displayprefetch&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;&lt;br /&gt;
* &#039;&#039;&#039;displaysize&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseFormatDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;canviewallsections&#039;&#039;&#039;: (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displayenabledownload&#039;&#039;&#039;: (optional) Whether the option to enable section/module download should be displayed. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displaysectionselector&#039;&#039;&#039;: (optional) Whether the default section selector should be displayed. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreUserDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039;: The type of the addon. Values accepted: &#039;newpage&#039; (default) or  &#039;communication&#039;. &lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreSettingsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for AddonMessageOutputDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
==Delegates==&lt;br /&gt;
&lt;br /&gt;
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
====CoreMainMenuDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). &lt;br /&gt;
&lt;br /&gt;
====CoreCourseOptionsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).&lt;br /&gt;
&lt;br /&gt;
====CoreCourseModuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting activity modules or resources.&lt;br /&gt;
&lt;br /&gt;
====CoreUserDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add additional options in the user profile page in the app.&lt;br /&gt;
&lt;br /&gt;
====CoreCourseFormatDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting course formats.&lt;br /&gt;
&lt;br /&gt;
====CoreSettingsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to add a new option in the settings page.&lt;br /&gt;
&lt;br /&gt;
====AddonMessageOutputDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a message output plugin.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question types.&lt;br /&gt;
https://docs.moodle.org/dev/Creating_mobile_question_types&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionBehaviourDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question behaviours.&lt;br /&gt;
&lt;br /&gt;
====CoreUserProfileFieldDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting user profile fields.&lt;br /&gt;
&lt;br /&gt;
====AddonModQuizAccessRuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a quiz access rule.&lt;br /&gt;
&lt;br /&gt;
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use these delegates to support assign submission or feedback plugins.&lt;br /&gt;
&lt;br /&gt;
====AddonWorkshopAssessmentStrategyDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a workshop assessment strategy plugin.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.&lt;br /&gt;
&lt;br /&gt;
* CoreContentLinksDelegate&lt;br /&gt;
* CoreCourseModulePrefetchDelegate&lt;br /&gt;
* CoreFileUploaderDelegate&lt;br /&gt;
* CorePluginFileDelegate&lt;br /&gt;
&lt;br /&gt;
==Available components and directives==&lt;br /&gt;
&lt;br /&gt;
===Difference between component and directives===&lt;br /&gt;
&lt;br /&gt;
A component (represented as an HTML tag) is used to add custom elements to the app.&lt;br /&gt;
Example of components are: ion-list, ion-item, core-search-box&lt;br /&gt;
&lt;br /&gt;
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.&lt;br /&gt;
Example of directives are: core-auto-focus, *ngIf, ng-repeat&lt;br /&gt;
&lt;br /&gt;
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:&lt;br /&gt;
* Angular directives, please check: https://angular.io/api?type=directive&lt;br /&gt;
* Ionic components, please check: https://ionicframework.com/docs/&lt;br /&gt;
&lt;br /&gt;
===Custom core components and directives===&lt;br /&gt;
&lt;br /&gt;
These are some useful custom components and directives (only available in the mobile app). Please notice that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.&lt;br /&gt;
&lt;br /&gt;
====core-format-text====&lt;br /&gt;
&lt;br /&gt;
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.&lt;br /&gt;
&lt;br /&gt;
This directive automatically applies core-external-content and core-link to all the links and embedded media.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;text&#039;&#039;&#039; (string): The text to format.&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* &#039;&#039;&#039;adaptImg&#039;&#039;&#039; (boolean): Optional. Whether to adapt images to screen width. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;clean&#039;&#039;&#039; (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;singleLine&#039;&#039;&#039; (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;maxHeight&#039;&#039;&#039; (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class=&amp;quot;inline&amp;quot; at the same time to use display: inline-block.&lt;br /&gt;
* &#039;&#039;&#039;fullOnClick&#039;&#039;&#039; (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;fullTitle&#039;&#039;&#039; (string): Optional. Title to use in full view. Defaults to &amp;quot;Description&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-format-text text=&amp;quot;&amp;lt;% cm.description %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-format-text&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-link====&lt;br /&gt;
&lt;br /&gt;
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;capture&#039;&#039;&#039; (boolean): Optional. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).&lt;br /&gt;
* &#039;&#039;&#039;inApp&#039;&#039;&#039; (boolean): Optional. True to open in embedded browser, false to open in system browser.&lt;br /&gt;
* &#039;&#039;&#039;autoLogin&#039;&#039;&#039; (string): Optional. If the link should be open with auto-login. Accepts the following values:&lt;br /&gt;
** &amp;quot;yes&amp;quot; -&amp;gt; Always auto-login.&lt;br /&gt;
** &amp;quot;no&amp;quot; -&amp;gt; Never auto-login.&lt;br /&gt;
** &amp;quot;check&amp;quot; -&amp;gt; Auto-login only if it points to the current site. Default value.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a href=&amp;quot;&amp;lt;% cm.url %&amp;gt;&amp;quot; core-link&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-external-content====&lt;br /&gt;
&lt;br /&gt;
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. &lt;br /&gt;
&lt;br /&gt;
If a file is downloaded, its URL will be replaced by the local file URL.&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;img src=&amp;quot;&amp;lt;% event.iconurl %&amp;gt;&amp;quot; core-external-content component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% event.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-user-link====&lt;br /&gt;
&lt;br /&gt;
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;userId&#039;&#039;&#039; (number): User id to open the profile.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): Optional. Course id to show the user info related to that course.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a ion-item core-user-link userId=&amp;quot;&amp;lt;% userid %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-file====&lt;br /&gt;
&lt;br /&gt;
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* file (object): The file. Must have a property &#039;filename&#039; and a &#039;fileurl&#039; or &#039;url&#039;&lt;br /&gt;
* component (string): Optional. Component the file belongs to.&lt;br /&gt;
* componentId (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* canDelete (boolean): Optional. Whether file can be deleted.&lt;br /&gt;
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they&#039;re outdated or not.&lt;br /&gt;
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-file [file]=&amp;quot;{fileurl: &#039;&amp;lt;% issue.url %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.name %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, filesize: &#039;&amp;lt;% issue.size %&amp;gt;&#039;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-file&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-download-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.&lt;br /&gt;
&lt;br /&gt;
It is usually recommended to use the core-file component since it also displays the state of the file.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;core-download-file&#039;&#039;&#039; (object): The file to download.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage: a button to download a file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button [core-download-file]=&amp;quot;{fileurl: &amp;lt;% issue.url %&amp;gt;, timemodified: &amp;lt;% issue.timemodified %&amp;gt;, filesize: &amp;lt;% issue.size %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.download | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-course-download-module-main-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and opening the main file of a module.&lt;br /&gt;
&lt;br /&gt;
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.&lt;br /&gt;
&lt;br /&gt;
This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;module&#039;&#039;&#039; (object): Optional. The module object. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;moduleId&#039;&#039;&#039; (number): Optional. The module ID. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): The course ID the module belongs to.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.&lt;br /&gt;
* &#039;&#039;&#039;files&#039;&#039;&#039; (object[]): Optional. List of files of the module. If not provided, use module.contents.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-navbar-buttons====&lt;br /&gt;
&lt;br /&gt;
Component to add buttons to the app&#039;s header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.&lt;br /&gt;
&lt;br /&gt;
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first &amp;lt;ion-buttons&amp;gt; found in the header.&lt;br /&gt;
&lt;br /&gt;
You can use the [hidden] input to hide all the inner buttons if a certain condition is met.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons end&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button icon-only (click)=&amp;quot;action()&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;ion-icon name=&amp;quot;funnel&amp;quot;&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also use this to add options to the context menu. Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons&amp;gt;&lt;br /&gt;
    &amp;lt;core-context-menu&amp;gt;&lt;br /&gt;
        &amp;lt;core-context-menu-item [priority]=&amp;quot;500&amp;quot; [content]=&amp;quot;&#039;Nice boat&#039;&amp;quot; (action)=&amp;quot;boatFunction()&amp;quot; [iconAction]=&amp;quot;&#039;boat&#039;&amp;quot;&amp;gt;&amp;lt;/core-context-menu-item&amp;gt;&lt;br /&gt;
    &amp;lt;/core-context-menu&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.&lt;br /&gt;
&lt;br /&gt;
===Specific component and directives for plugins===&lt;br /&gt;
&lt;br /&gt;
These are component and directives created specifically for supporting Moodle plugins.&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the new &#039;&#039;get_content&#039;&#039; WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to go to a new content page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to load new content in current page using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.&lt;br /&gt;
&lt;br /&gt;
If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;successMessage&#039;&#039;&#039; (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).&lt;br /&gt;
* &#039;&#039;&#039;goBackOnSuccess&#039;&#039;&#039; (boolean): Whether to go back if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;refreshOnSuccess&#039;&#039;&#039; (boolean): Whether to refresh the current view if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server without using cache, displaying default messages and refreshing on success:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage successMessage refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; goBackOnSuccess=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;certificateViewed($event)&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.certificateViewed = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t need to load some new content when done, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;jsData&#039;&#039;&#039; (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;newContentPreSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server without using cache, showing default confirm and displaying a new page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server using cache, without confirm, displaying new content in same page and using &#039;&#039;userid&#039;&#039; from &#039;&#039;otherdata&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-on-load====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.&lt;br /&gt;
&lt;br /&gt;
If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Advanced features==&lt;br /&gt;
&lt;br /&gt;
===Display the plugin only if certain conditions are met===&lt;br /&gt;
&lt;br /&gt;
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the &amp;quot;init&amp;quot; method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).&lt;br /&gt;
&lt;br /&gt;
All the init methods are called as soon as your plugin is retrieved. If you don&#039;t want your plugin to be displayed for the current user, then you should return an exception in this init method. It&#039;s recommended to include a message explaining why the plugin isn&#039;t available for the current user, this exception will be logged in the Javascript console.&lt;br /&gt;
&lt;br /&gt;
On the other hand, you might want to display a plugin only for certain courses (&#039;&#039;CoreCourseOptionsDelegate&#039;&#039;) or only if the user is viewing certain users&#039; profiles (&#039;&#039;CoreUserDelegate&#039;&#039;). This can be achieved with the init method too.&lt;br /&gt;
&lt;br /&gt;
In the init method you can return a &amp;quot;restrict&amp;quot; property with two fields in it: &#039;&#039;courses&#039;&#039; and &#039;&#039;users&#039;&#039;. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users&#039; profiles.&lt;br /&gt;
&lt;br /&gt;
===Using “otherdata”===&lt;br /&gt;
&lt;br /&gt;
The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a &#039;&#039;get_content&#039;&#039; WS call is added to a variable named CONTENT_OTHERDATA.&lt;br /&gt;
&lt;br /&gt;
The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.&lt;br /&gt;
&lt;br /&gt;
This means that, in your Javascript, you can access and use these data like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
this.CONTENT_OTHERDATA.myVar&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
And in the template you could use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
{{ CONTENT_OTHERDATA.myVar }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;myVar&#039;&#039; is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:&lt;br /&gt;
&lt;br /&gt;
array(&#039;myVar&#039; =&amp;gt; &#039;Initial value&#039;)&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.&lt;br /&gt;
&lt;br /&gt;
We will return the initial value of the input in the otherdata of our PHP method:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;myVar&#039; =&amp;gt; &#039;My initial value&#039;),&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then in the template we will use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-item text-wrap&amp;gt;&lt;br /&gt;
    &amp;lt;ion-label stacked&amp;gt;{{ &#039;plugin.mod_certificate.textlabel | translate }}&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;text&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.myVar&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [useOtherDataForWS]=&amp;quot;[&#039;myVar&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
        {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we are creating an input text and we use &#039;&#039;[(ngModel)]&#039;&#039; to use the value in &#039;&#039;myVar&#039;&#039; as the initial value and to store the changes in the same &#039;&#039;myVar&#039;&#039; variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the &#039;&#039;myVar&#039;&#039; variable. This is called 2-way data binding in Angular.&lt;br /&gt;
&lt;br /&gt;
Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the &#039;&#039;useOtherDataForWS&#039;&#039; attribute to specify which variable from &#039;&#039;otherdata&#039;&#039; we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService &#039;&#039;mod_certificate_my_webservice&#039;&#039; and will send as a param: myVar -&amp;gt; “A new value”.&lt;br /&gt;
&lt;br /&gt;
We can achieve the same result using the &#039;&#039;params&#039;&#039; attribute of the core-site-plugins-call-ws directive instead of using &#039;&#039;useOtherDataForWS&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [params]=&amp;quot;{myVar: CONTENT_OTHERDATA.myVar}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The WebService call will be exactly the same with both buttons.&lt;br /&gt;
&lt;br /&gt;
Please notice that this example could be done without using otherdata too, using the “&#039;&#039;form&#039;&#039;” input of the &#039;&#039;core-site-plugins-call-ws directive&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Running JS code after a content template has loaded===&lt;br /&gt;
&lt;br /&gt;
When you return JavaScript code from a handler function using the &#039;javascript&#039; array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. &lt;br /&gt;
&lt;br /&gt;
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;template&#039; =&amp;gt; [ ... ],&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;setTimeout(function() { console.log(&amp;quot;DOM is available now&amp;quot;); });&#039;,&lt;br /&gt;
    &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
    &#039;files&#039; =&amp;gt; []&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
===JS functions visible in the templates===&lt;br /&gt;
&lt;br /&gt;
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;openContent(title: string, args: any, component?: string, method?: string)&#039;&#039;&#039;: Open a new page to display some new content. You need to specify the &#039;&#039;title&#039;&#039; of the new page and the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
* &#039;&#039;&#039;refreshContent(showSpinner = true)&#039;&#039;&#039;: Refresh the current content. By default it will display a spinner while refreshing, if you don&#039;t want it to be displayed you should pass false as a parameter.&lt;br /&gt;
* &#039;&#039;&#039;updateContent(args: any, component?: string, method?: string)&#039;&#039;&#039;: Refresh the current content using different params. You need to specify the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Group selector=====&lt;br /&gt;
&lt;br /&gt;
Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.&lt;br /&gt;
&lt;br /&gt;
To do so, we&#039;ll use a drop down to select the group. When the user selects a group using this drop down we&#039;ll update the page content to display the new group.&lt;br /&gt;
&lt;br /&gt;
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular&#039;s &#039;&#039;ngModel&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
======Using plain HTML======&lt;br /&gt;
&lt;br /&gt;
We need to add a &amp;quot;&#039;&#039;selected&#039;&#039;&amp;quot; attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
        // Detect which group is selected.&lt;br /&gt;
        foreach ($groups as $gid=&amp;gt;$group) {&lt;br /&gt;
            $group-&amp;gt;selected = $gid === $groupid;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid,&lt;br /&gt;
            &#039;groups&#039; =&amp;gt; $groups&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the code above, we&#039;re retrieving the groups the user can see and then we&#039;re adding a &amp;quot;selected&amp;quot; bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.&lt;br /&gt;
&lt;br /&gt;
In the template, we display the drop down like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: $event})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot; &amp;lt;%#selected%&amp;gt;selected&amp;lt;%/selected%&amp;gt; &amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;ionChange&#039;&#039; function will be called everytime the user selects a different group with the drop down. We&#039;re using the function &#039;&#039;updateContent&#039;&#039; to update the current view using the new group. &#039;&#039;$event&#039;&#039; is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.&lt;br /&gt;
&lt;br /&gt;
======Using ngModel======&lt;br /&gt;
&lt;br /&gt;
ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn&#039;t have &#039;&#039;ng-init&#039;&#039; like in AngularJS), so we&#039;ll use &amp;quot;otherdata&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
In the PHP function we&#039;ll return the group that needs to be selected in the &#039;&#039;otherdata&#039;&#039; array:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
         return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; array(&lt;br /&gt;
                &#039;group&#039; =&amp;gt; $groupid&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we don&#039;t need to iterate over the groups array like in the plain HTML example. However, now we&#039;re returning the groupid in the &amp;quot;otherdata&amp;quot; array. As it&#039;s explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using &amp;quot;otherdata&amp;quot;]] section, this &amp;quot;otherdata&amp;quot; is visible in the templates inside a variable named &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. So in the template we&#039;ll use this variable like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.group&amp;quot; (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: CONTENT_OTHERDATA.group})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Use the rich text editor===&lt;br /&gt;
&lt;br /&gt;
The rich text editor included in the app requires a FormControl to work. You can use the library FormBuilder to create this control (or to create a whole FormGroup if you prefer).&lt;br /&gt;
&lt;br /&gt;
With the following Javascript you&#039;ll be able to create a FormControl:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we&#039;re using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.&lt;br /&gt;
&lt;br /&gt;
Then you need to pass this control to the rich text editor in your template:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;core-rich-text-editor item-content [control]=&amp;quot;control&amp;quot; placeholder=&amp;quot;Enter your text here&amp;quot; name=&amp;quot;rte_answer&amp;quot;&amp;gt;&amp;lt;/core-rich-text-editor&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, there are several ways to send the value in the rich text editor to a WebService to save it. This is one of the simplest options:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_webservice&amp;quot; [params]=&amp;quot;{rte: control.value}&amp;quot; ....&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re passing the value of the rich text editor as a parameter to our WebService.&lt;br /&gt;
&lt;br /&gt;
===Initialization===&lt;br /&gt;
&lt;br /&gt;
All handlers can specify a “&#039;&#039;init&#039;&#039;” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.&lt;br /&gt;
&lt;br /&gt;
When the app retrieves all the handlers, the first thing it will do is call the &#039;&#039;tool_mobile_get_content&#039;&#039; WebService with the init method. This WS call will only receive the default args.&lt;br /&gt;
&lt;br /&gt;
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.&lt;br /&gt;
&lt;br /&gt;
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.INIT_TEMPLATES[‘main’];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
In this case, “main” is the ID of the template we want to use.&lt;br /&gt;
&lt;br /&gt;
The same happens with the &#039;&#039;otherdata&#039;&#039; returned by this init method, it is added to a INIT_OTHERDATA variable.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;restrict&#039;&#039; field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate &#039;&#039;CoreCourseOptionsDelegate&#039;&#039; and you return a list of courseids in restrict-&amp;gt;courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.&lt;br /&gt;
&lt;br /&gt;
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    MyAddonClass: new MyAddonClass()&lt;br /&gt;
};&lt;br /&gt;
result:&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.MyAddonClass&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Module link handler=====&lt;br /&gt;
&lt;br /&gt;
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function AddonModCertificateModuleLinkHandler() {&lt;br /&gt;
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, &#039;mmaModCertificate&#039;, &#039;certificate&#039;);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateLinkHandler&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;&lt;br /&gt;
&lt;br /&gt;
this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Advanced link handler=====&lt;br /&gt;
Link handlers have some advanced features that allow you to change how links behave under different conditions.&lt;br /&gt;
======Patterns======&lt;br /&gt;
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.pattern = RegExp(&#039;\/mod\/foo\/specialpage.php&#039;);&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Priority======&lt;br /&gt;
Multiple link handlers may apply to a given link.  You can define the order of precedence by setting the priority - the handler with the highest priority will be used.&lt;br /&gt;
All default handlers have a priority of 0, so 1 or higher will override the default.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Multiple actions======&lt;br /&gt;
Once a link has been matched, the handler&#039;s getActions() method determines what the link should do.  This method has access to the URL and its parameters.&lt;br /&gt;
Different actions can be returned depending on different conditions.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {&lt;br /&gt;
    return [{&lt;br /&gt;
        action: function(siteId, navCtrl) {&lt;br /&gt;
            // The actual behaviour of the link goes here.&lt;br /&gt;
        },&lt;br /&gt;
        sites: [...]&lt;br /&gt;
    }, {&lt;br /&gt;
        ...&lt;br /&gt;
    }];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order.  The first &amp;quot;valid&amp;quot; action will be used to open the link.&lt;br /&gt;
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can &amp;quot;invalidate&amp;quot;&lt;br /&gt;
your action by settings its sites propety to an empty array.&lt;br /&gt;
======Complex example======&lt;br /&gt;
This will match all URLs containing /mod/foo/, and force those with an id parameter that&#039;s not in the &amp;quot;supportedModFoos&amp;quot; array to open in the user&#039;s browser, rather than the app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
var supportedModFoos = [...];&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    this.pattern = new RegExp(&#039;\/mod\/foo\/&#039;);&lt;br /&gt;
    this.name = &amp;quot;AddonModFooLinkHandler&amp;quot;;&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
}&lt;br /&gt;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);&lt;br /&gt;
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     &lt;br /&gt;
    var action = {&lt;br /&gt;
        action: function() {&lt;br /&gt;
            that.CoreUtilsProvider.openInBrowser(url);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {&lt;br /&gt;
        action.sites = [];&lt;br /&gt;
    }&lt;br /&gt;
    return [action];&lt;br /&gt;
};&lt;br /&gt;
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Module prefetch handler=====&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;CoreCourseModuleDelegate&#039;&#039; handler allows you to define a list of &#039;&#039;offlinefunctions&#039;&#039; to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using &#039;&#039;offlinefunctions&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to create a prefetch handler using init JS:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
// Create a class that &amp;quot;inherits&amp;quot; from CoreCourseActivityPrefetchHandlerBase.&lt;br /&gt;
function AddonModCertificateModulePrefetchHandler() {&lt;br /&gt;
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,&lt;br /&gt;
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateModulePrefetchHandler&amp;quot;;&lt;br /&gt;
    this.modName = &amp;quot;certificate&amp;quot;;&lt;br /&gt;
    this.component = &amp;quot;mod_certificate&amp;quot;; // This must match the plugin identifier from db/mobile.php, otherwise the download link in the context menu will not update correctly.&lt;br /&gt;
    this.updatesNames = /^configuration$|^.*files$/;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;&lt;br /&gt;
&lt;br /&gt;
// Override the prefetch call.&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {&lt;br /&gt;
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function prefetchCertificate(module, courseId, single, siteId) {&lt;br /&gt;
    // Perform all the WS calls.&lt;br /&gt;
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won&#039;t be able to prefetch all the possible parameters. &lt;br /&gt;
&lt;br /&gt;
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom &#039;section&#039; parameter for the mobile function &#039;mobile_document_view&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function prefetchOucontent(module, courseId, single, siteId) {&lt;br /&gt;
    var component = &#039;mod_oucontent&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Get the site, first.&lt;br /&gt;
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {&lt;br /&gt;
        // Read the list of pages in this document using a web service.&lt;br /&gt;
        return site.read(&#039;mod_oucontent_get_page_list&#039;, {&#039;cmid&#039;: module.id}).then(function(response) {&lt;br /&gt;
            var promises = [];&lt;br /&gt;
&lt;br /&gt;
            // For each page, read and process the page - this is a copy of logic in the app at&lt;br /&gt;
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.&lt;br /&gt;
            for(var i = 0; i &amp;lt; response.length; i++) {&lt;br /&gt;
                var args = {&lt;br /&gt;
                    courseid: courseId,&lt;br /&gt;
                    cmid: module.id,&lt;br /&gt;
                    userid: site.getUserId()&lt;br /&gt;
                };&lt;br /&gt;
                if (response[i] !== &#039;&#039;) {&lt;br /&gt;
                    args.section = response[i];&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                promises.push(that.CoreSitePluginsProvider.getContent(&lt;br /&gt;
                        component, &#039;mobile_document_view&#039;, args).then(&lt;br /&gt;
                        function(result) {&lt;br /&gt;
                            var subPromises = [];&lt;br /&gt;
                            if (result.files &amp;amp;&amp;amp; result.files.length) {&lt;br /&gt;
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(&lt;br /&gt;
                                        site.id, result.files, true, false, component, module.id));&lt;br /&gt;
                            }&lt;br /&gt;
                            return Promise.all(subPromises);&lt;br /&gt;
                        }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            return Promise.all(promises);&lt;br /&gt;
        });&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Single activity course format=====&lt;br /&gt;
&lt;br /&gt;
In the following example, the value of INIT_TEMPLATES[&amp;quot;main&amp;quot;] is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;core-dynamic-component [component]=&amp;quot;componentClass&amp;quot; [data]=&amp;quot;data&amp;quot;&amp;gt;&amp;lt;/core-dynamic-component&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This template is returned by the init method. And this is the JavaScript code returned by the init method:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function getAddonSingleActivityFormatComponent() {&lt;br /&gt;
    function AddonSingleActivityFormatComponent() {&lt;br /&gt;
        this.data = {};&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {&lt;br /&gt;
        var self = this;&lt;br /&gt;
&lt;br /&gt;
        if (this.course &amp;amp;&amp;amp; this.sections &amp;amp;&amp;amp; this.sections.length) {&lt;br /&gt;
            var module = this.sections[0] &amp;amp;&amp;amp; this.sections[0].modules &amp;amp;&amp;amp; this.sections[0].modules[0];&lt;br /&gt;
            if (module &amp;amp;&amp;amp; !this.componentClass) {&lt;br /&gt;
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) =&amp;gt; {&lt;br /&gt;
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.data.courseId = this.course.id;&lt;br /&gt;
            this.data.module = module;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {&lt;br /&gt;
        return Promise.resolve(this.dynamicComponent.callComponentFunction(&amp;quot;doRefresh&amp;quot;, [refresher, done]));&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
return AddonSingleActivityFormatComponent;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function AddonSingleActivityFormatHandler() {&lt;br /&gt;
    this.name = &amp;quot;singleactivity&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.isEnabled = function() {&lt;br /&gt;
    return true;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {&lt;br /&gt;
    if (sections &amp;amp;&amp;amp; sections[0] &amp;amp;&amp;amp; sections[0].modules &amp;amp;&amp;amp; sections[0].modules[0]) {&lt;br /&gt;
        return sections[0].modules[0].name;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return course.fullname || &amp;quot;&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {&lt;br /&gt;
    that.Injector = injector || that.Injector;&lt;br /&gt;
&lt;br /&gt;
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES[&amp;quot;main&amp;quot;], getAddonSingleActivityFormatComponent(), injector);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using the JavaScript API===&lt;br /&gt;
&lt;br /&gt;
The Javascript API is partly supported right now, only the delegates specified in the section [[Mobile_support_for_plugins#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] supports it now. This API allows you to override any of the functions of the default handler. &lt;br /&gt;
&lt;br /&gt;
The “method” specified in a handler registered in the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.&lt;br /&gt;
&lt;br /&gt;
For example, if the Javascript returned by the method returns something like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The the &#039;&#039;getData&#039;&#039; function of the default handler will be overridden by the returned getData function.&lt;br /&gt;
&lt;br /&gt;
The default handler for &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; only has 2 functions: &#039;&#039;getComponent&#039;&#039; and &#039;&#039;getData&#039;&#039;. In addition, the JavaScript code can return an extra function named &#039;&#039;componentInit&#039;&#039; that will be executed when the component returned by &#039;&#039;getComponent&#039;&#039; is initialized.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to support the text user profile field using this API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
var result = {&lt;br /&gt;
    componentInit: function() {&lt;br /&gt;
        if (this.field &amp;amp;&amp;amp; this.edit &amp;amp;&amp;amp; this.form) {&lt;br /&gt;
            this.field.modelName = &amp;quot;profile_field_&amp;quot; + this.field.shortname;&lt;br /&gt;
&lt;br /&gt;
            if (this.field.param2) {&lt;br /&gt;
                this.field.maxlength = parseInt(this.field.param2, 10) || &amp;quot;&amp;quot;;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? &amp;quot;password&amp;quot; : &amp;quot;text&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var formData = {&lt;br /&gt;
                value: this.field.defaultdata,&lt;br /&gt;
                disabled: this.disabled&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required &amp;amp;&amp;amp; !this.field.locked ? that.Validators.required : null));&lt;br /&gt;
        }&lt;br /&gt;
    },&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
        var name = &amp;quot;profile_field_&amp;quot; + field.shortname;&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            type: &amp;quot;text&amp;quot;,&lt;br /&gt;
            name: name,&lt;br /&gt;
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name])&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Translate dynamic strings===&lt;br /&gt;
&lt;br /&gt;
If you wish to have an element that displays a localised string based on value from your template you can doing something like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content translate&amp;gt;&lt;br /&gt;
        plugin.mod_myactivity.&amp;lt;% status %&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This could save you from having to write something like when only one value should be displayed:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content&amp;gt;&lt;br /&gt;
        &amp;lt;%#isedting%&amp;gt;{{ &#039;plugin.mod_myactivity.editing&#039; | translate }}&amp;lt;%/isediting%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isopen%&amp;gt;{{ &#039;plugin.mod_myactivity.open&#039; | translate }}&amp;lt;%/isopen%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isclosed%&amp;gt;{{ &#039;plugin.mod_myactivity.closed&#039; | translate }}&amp;lt;%/isclosed%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
=== Invalid response received ===&lt;br /&gt;
&lt;br /&gt;
You might receive this error when using the &amp;quot;core-site-plugins-call-ws&amp;quot; directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{typeExpected: &#039;boolean&#039;}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{responseExpected: false}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS ===&lt;br /&gt;
&lt;br /&gt;
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don&#039;t use HTML inputs, they simulate them, so the directive isn&#039;t going to find their data and so it won&#039;t be sent to the WebService.&lt;br /&gt;
&lt;br /&gt;
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.&lt;br /&gt;
&lt;br /&gt;
==== Sending the data manually ====&lt;br /&gt;
&lt;br /&gt;
The first solution is to send the missing params manually using the &amp;quot;&#039;&#039;params&#039;&#039;&amp;quot; property. We will use &#039;&#039;ngModel&#039;&#039; to store the input value in a variable, and this variable will be passed to the params. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a template like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group name=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, responses: responses}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Basically, you need to add &#039;&#039;ngModel&#039;&#039; to the affected element (in this case, the &#039;&#039;radio-group&#039;&#039;). You can put whatever name you want as the value, we used &amp;quot;responses&amp;quot;. With this, everytime the user selects a radio button the value will be stored in a variable named &amp;quot;responses&amp;quot;. Then, in the button we are passing this variable to the params of the WebService.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &amp;quot;form&amp;quot; attribute has priority over &amp;quot;params&amp;quot;, so if you have an input with name=&amp;quot;responses&amp;quot; it will override what you&#039;re manually passing to params.&lt;br /&gt;
&lt;br /&gt;
==== Using a hidden input ====&lt;br /&gt;
&lt;br /&gt;
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use &#039;&#039;ngModel&#039;&#039; to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a radio button like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot; [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;hidden&amp;quot; [ngModel]=&amp;quot;responses&amp;quot; name=&amp;quot;responses&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we&#039;re using a variable named &amp;quot;responses&amp;quot; to synchronize the data between the &#039;&#039;radio-group&#039;&#039; and the hidden input. You can use whatever name you want.&lt;br /&gt;
&lt;br /&gt;
=== I can&#039;t return an object or array in otherdata ===&lt;br /&gt;
&lt;br /&gt;
If you try to return an object or an array in any field inside &#039;&#039;otherdata&#039;&#039;, the WebService call will fail with the following error:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Scalar type expected, array or object received&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Each field in &#039;&#039;otherdata&#039;&#039; must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($data))&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The app will automatically parse this JSON and convert it back into an array or object.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===Accepting dynamic names in a WebService===&lt;br /&gt;
&lt;br /&gt;
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.&lt;br /&gt;
&lt;br /&gt;
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they&#039;re dynamic. The solution is to accept an array of objects with name and value. So in the &#039;&#039;_parameters()&#039;&#039; function of our new WebService, we will add this parameter:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;data&#039; =&amp;gt; new external_multiple_structure(&lt;br /&gt;
     new external_single_structure(&lt;br /&gt;
        array(&lt;br /&gt;
            &#039;name&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data name&#039;),&lt;br /&gt;
            &#039;value&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data value&#039;),&lt;br /&gt;
        )&lt;br /&gt;
    ),&lt;br /&gt;
    &#039;The data to be saved&#039;, VALUE_DEFAULT, array()&lt;br /&gt;
)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive &#039;&#039;core-site-plugins-call-ws&#039;&#039; that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the &amp;quot;&#039;&#039;form&#039;&#039;&amp;quot; attribute, because we need to format the data before it is sent.&lt;br /&gt;
&lt;br /&gt;
Since we will send the params manually and we want it all to be sent in the same array, we will use &#039;&#039;ngModel&#039;&#039; to store the input data into a variable that we&#039;ll call &amp;quot;data&amp;quot;, but you can use the name you want. This &amp;quot;data&amp;quot; will be an object that will hold the input data with the format &amp;quot;name-&amp;gt;value&amp;quot;. For example, if I have an input with name &amp;quot;a1&amp;quot; and value &amp;quot;My answer&amp;quot;, the data object will be:&lt;br /&gt;
&lt;br /&gt;
{a1: &amp;quot;My answer&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
So we need to add &#039;&#039;ngModel&#039;&#039; to all the inputs whose values need to be sent to the &amp;quot;data&amp;quot; WS param. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&amp;lt;ion-input name=&amp;quot;&amp;lt;% name %&amp;gt;&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.data[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re using &#039;&#039;CONTENT_OTHERDATA&#039;&#039; to store the data. We do it like this because we&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the form, setting the values the user has already stored. If you don&#039;t need to initialize the form, then you can use the variable &amp;quot;dataObject&amp;quot;, an empty object that the Mobile app creates for you: [(ngModel)]=&amp;quot;dataObject[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: &#039;&#039;objectToArrayOfObjects&#039;&#039;. So in our button we&#039;ll use this function to format the data before it&#039;s sent:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_ws_name&amp;quot;&lt;br /&gt;
    [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, &#039;name&#039;, &#039;value&#039;)}&amp;quot;&lt;br /&gt;
    successMessage&lt;br /&gt;
    refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see in the example above, we&#039;re specifying that the keys of the &amp;quot;data&amp;quot; object need to be stored in a property named &amp;quot;name&amp;quot;, and the values need to be stored in a property named &amp;quot;value&amp;quot;. If your WebService expects different names you need to change the parameters of the function &#039;&#039;objectToArrayOfObjects&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable &amp;quot;data&amp;quot; doesn&#039;t exist inside &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. As it is explained in previous sections, &#039;&#039;CONTENT_OTHERDATA&#039;&#039; holds the data that you return in &#039;&#039;otherdata&#039;&#039; for your method. We&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the values to be displayed in the form.&lt;br /&gt;
&lt;br /&gt;
If the user hasn&#039;t answered the form yet, we can initialize the &amp;quot;data&amp;quot; object as an empty object. Please remember that we cannot return arrays or objects in &#039;&#039;otherdata&#039;&#039;, so we&#039;ll return a JSON string.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; &#039;{}&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$userdata = get_user_responses(); // It will held the data in a format name-&amp;gt;value. Example: array(&#039;a1&#039; =&amp;gt; &#039;My value&#039;).&lt;br /&gt;
...&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($userdata))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.&lt;br /&gt;
&lt;br /&gt;
==Moodle plugins with mobile support==&lt;br /&gt;
&lt;br /&gt;
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].&lt;br /&gt;
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].&lt;br /&gt;
* Gapfill question type: [https://moodle.org/plugins/qtype_gapfill Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_gapfill in github].&lt;br /&gt;
* Wordselect question type: [https://moodle.org/plugins/qtype_wordselect Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_wordselect in github].&lt;br /&gt;
* RegExp question type: [https://moodle.org/plugins/qtype_regexp Moodle plugins directory entry] and [https://github.com/rezeau/moodle-qtype_regexp in github].&lt;br /&gt;
* Certificate: [https://moodle.org/plugins/mod_certificate Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_certificate in github].&lt;br /&gt;
* Attendance [https://moodle.org/plugins/mod_attendance Moodle plugins directory entry] and [https://github.com/danmarsden/moodle-mod_attendance in github].&lt;br /&gt;
&lt;br /&gt;
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&amp;amp;id=6 here] (it may contain some outdated plugins)&lt;br /&gt;
[[Category:Mobile]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Calendar_API&amp;diff=54662</id>
		<title>Calendar API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Calendar_API&amp;diff=54662"/>
		<updated>2018-08-24T08:55:57Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: Clarify the use of $checkcapability parameter in event creation and updating&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{ Moodle 3.3 }}&lt;br /&gt;
&lt;br /&gt;
This page documents the Calendar API as it is in Moodle 3.3 and later. For the API in older versions of Moodle, see [[Calendar_API_old]].&lt;br /&gt;
&lt;br /&gt;
The Calendar API allows you to add, modify and delete events in the calendar for user, groups, courses and the site. As of 3.3 it also allows you to provide actions for these events so that they are then displayed on block_myoverview, which by default is shown on users&#039; dashboard.&lt;br /&gt;
&lt;br /&gt;
==Overview==&lt;br /&gt;
&lt;br /&gt;
The Moodle [[:en:Calendar|Calendar]] collects and displays calendar events to users. These events are generated by other plugins, like activities, to let the user know of an important date. For example, when an assignment opens for submission.&lt;br /&gt;
&lt;br /&gt;
The block_myoverview plugin displays calendar events that have an action associated with them. For example, an activity may have a due date specified, in which case it will create a calendar action event so that the event will display on the dashboard for the user, as well as the calendar. In order to provide the action associated for this event you have to define a callback in your plugin which is detailed below.&lt;br /&gt;
&lt;br /&gt;
==Creating an event==&lt;br /&gt;
Creating a new calendar event. The optional parameter $checkcapability is used to check user&#039;s capability to add events. By default the $checkcapability parameter is set to true. You should set it to false if you have already checked that the user has the capabilities required for the event to be created, for example when an activity is creating an event based on a deadline.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
require_once($CFG-&amp;gt;dirroot.&#039;/calendar/lib.php&#039;);&lt;br /&gt;
&lt;br /&gt;
$event = new stdClass();&lt;br /&gt;
$event-&amp;gt;eventtype = SCORM_EVENT_TYPE_OPEN; // Constant defined somewhere in your code - this can be any string value you want. It is a way to identify the event.&lt;br /&gt;
$event-&amp;gt;type = CALENDAR_EVENT_TYPE_STANDARD; // This is used for events we only want to display on the calendar, and are not needed on the block_myoverview.&lt;br /&gt;
$event-&amp;gt;name = get_string(&#039;calendarstart&#039;, &#039;scorm&#039;, $scorm-&amp;gt;name);&lt;br /&gt;
$event-&amp;gt;description = format_module_intro(&#039;scorm&#039;, $scorm, $cmid);&lt;br /&gt;
$event-&amp;gt;courseid = $scorm-&amp;gt;course;&lt;br /&gt;
$event-&amp;gt;groupid = 0;&lt;br /&gt;
$event-&amp;gt;userid = 0;&lt;br /&gt;
$event-&amp;gt;modulename = &#039;scorm&#039;;&lt;br /&gt;
$event-&amp;gt;instance = $scorm-&amp;gt;id;&lt;br /&gt;
$event-&amp;gt;timestart = $scorm-&amp;gt;timeopen;&lt;br /&gt;
$event-&amp;gt;visible = instance_is_visible(&#039;scorm&#039;, $scorm);&lt;br /&gt;
$event-&amp;gt;timeduration = 0;&lt;br /&gt;
&lt;br /&gt;
calendar_event::create($event);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Updating an event==&lt;br /&gt;
You can update an existing event in database by providing at least the event id. If the event is a part of a chain of repeated events, the rest of series event will also be updated (depending on the value of property repeateditall). This function could also be used to insert new event to database, if the given event does not exist yet. The optional parameter $checkcapability is used to check user&#039;s capability to edit/add events. By default the $checkcapability parameter is set to true. You should set it to false if you have already checked that the user has the capabilities required for the event to be updated, for example when an activity is updating an event based on a change to it&#039;s settings.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$eventid = required_param(&#039;id&#039;, PARAM_INT);&lt;br /&gt;
$event = calendar_event::load($eventid);&lt;br /&gt;
&lt;br /&gt;
$data = $mform-&amp;gt;get_data();&lt;br /&gt;
$event-&amp;gt;update($data);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Deleting an event==&lt;br /&gt;
You can delete an existing event from the database. The optional parameter $deleterepeated is used as an indicator to remove the rest of repeated events. The default value for $deleterepeated is true. Deleting an event will also delete all associated files related to the event&#039;s editor context.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$eventid = required_param(&#039;id&#039;, PARAM_INT);&lt;br /&gt;
$event = calendar_event::load($eventid);&lt;br /&gt;
$event-&amp;gt;delete($repeats);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Event priority==&lt;br /&gt;
There might be cases that an activity event will have user and/or group overrides. Therefore we need a way to show only the relevant event on the user&#039;s calendar. This is where the &#039;priority&#039; field comes in. &lt;br /&gt;
&lt;br /&gt;
The event priority is set to the following:&lt;br /&gt;
* NULL for non-override events.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$event-&amp;gt;priority = null;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
* 0 for user override events.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$event-&amp;gt;priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
* A positive integer for group events.&lt;br /&gt;
&lt;br /&gt;
For integer and non-null event priorities, the lower the value, the higher the priority is. Meaning, user overrides always have a higher priority than group overrides. Group override priorities are currently being determined in two ways in core activities:&lt;br /&gt;
# In the assignment module, the event priorities for group overrides are being determined from the &#039;sortorder&#039; column in the &#039;assign_overrides&#039; table.&lt;br /&gt;
# In the lesson and quiz modules, the event priorities for group overrides are being calculated using the functions lesson_get_group_override_priorities($lessonid) and quiz_get_group_override_priorities($quizid).&lt;br /&gt;
&lt;br /&gt;
Should you ever decide to sort out group override priorities by implementing *_get_group_override_priorities(), the recommended return structure would be something like&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
[&lt;br /&gt;
    &#039;youreventtype1&#039; =&amp;gt; $prioritiesforeventtype1, &lt;br /&gt;
    ...&lt;br /&gt;
]&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
where &#039;$prioritiesforeventtype1&#039; is an associative array that has the timestamp of the group override event as key and the calculated priority as value. For more details, please see the implementation for the lesson module below:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Calculates the priorities of timeopen and timeclose values for group overrides for a lesson.&lt;br /&gt;
 *&lt;br /&gt;
 * @param int $lessonid The lesson ID.&lt;br /&gt;
 * @return array|null Array of group override priorities for open and close times. Null if there are no group overrides.&lt;br /&gt;
 */&lt;br /&gt;
function lesson_get_group_override_priorities($lessonid) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    // Fetch group overrides.&lt;br /&gt;
    $where = &#039;lessonid = :lessonid AND groupid IS NOT NULL&#039;;&lt;br /&gt;
    $params = [&#039;lessonid&#039; =&amp;gt; $lessonid];&lt;br /&gt;
    $overrides = $DB-&amp;gt;get_records_select(&#039;lesson_overrides&#039;, $where, $params, &#039;&#039;, &#039;id, groupid, available, deadline&#039;);&lt;br /&gt;
    if (!$overrides) {&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $grouptimeopen = [];&lt;br /&gt;
    $grouptimeclose = [];&lt;br /&gt;
    foreach ($overrides as $override) {&lt;br /&gt;
        if ($override-&amp;gt;available !== null &amp;amp;&amp;amp; !in_array($override-&amp;gt;available, $grouptimeopen)) {&lt;br /&gt;
            $grouptimeopen[] = $override-&amp;gt;available;&lt;br /&gt;
        }&lt;br /&gt;
        if ($override-&amp;gt;deadline !== null &amp;amp;&amp;amp; !in_array($override-&amp;gt;deadline, $grouptimeclose)) {&lt;br /&gt;
            $grouptimeclose[] = $override-&amp;gt;deadline;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Sort open times in ascending manner. The earlier open time gets higher priority.&lt;br /&gt;
    sort($grouptimeopen);&lt;br /&gt;
    // Set priorities.&lt;br /&gt;
    $opengrouppriorities = [];&lt;br /&gt;
    $openpriority = 1;&lt;br /&gt;
    foreach ($grouptimeopen as $timeopen) {&lt;br /&gt;
        $opengrouppriorities[$timeopen] = $openpriority++;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Sort close times in descending manner. The later close time gets higher priority.&lt;br /&gt;
    rsort($grouptimeclose);&lt;br /&gt;
    // Set priorities.&lt;br /&gt;
    $closegrouppriorities = [];&lt;br /&gt;
    $closepriority = 1;&lt;br /&gt;
    foreach ($grouptimeclose as $timeclose) {&lt;br /&gt;
        $closegrouppriorities[$timeclose] = $closepriority++;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return [&lt;br /&gt;
        &#039;open&#039; =&amp;gt; $opengrouppriorities,&lt;br /&gt;
        &#039;close&#039; =&amp;gt; $closegrouppriorities&lt;br /&gt;
    ];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Action events==&lt;br /&gt;
&lt;br /&gt;
Action events are calendar events that can be actioned. E.g. A student submitting an assignment by a certain date. These events are displayed on the block_myoverview which by default is on users&#039; dashboard. Creating these is the same as creating a normal calendar event except instead of using CALENDAR_EVENT_TYPE_STANDARD as your calendar event type, you use CALENDAR_EVENT_TYPE_ACTION. The events are also sorted on the dashboard by the value specified in the &#039;timesort&#039; field (unixtime) for the event.&lt;br /&gt;
&lt;br /&gt;
Example of the changes to the above code would be to change the &#039;type&#039; and to specify the &#039;timesort&#039; value.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$event-&amp;gt;type = CALENDAR_EVENT_TYPE_ACTION;&lt;br /&gt;
$event-&amp;gt;timesort = $scorm-&amp;gt;timeclose;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:my overview sam student.png]]&lt;br /&gt;
&lt;br /&gt;
===The callbacks===&lt;br /&gt;
&lt;br /&gt;
There are 3 callbacks your module can implement that are used to control when and how your action is shown to the user.&lt;br /&gt;
&lt;br /&gt;
====mod_xyz_core_calendar_is_event_visible()====&lt;br /&gt;
This callback determines if an event should be visible throughout the site. For example, the assignment module creates a grading event for teachers. We do not want this event being visible to users who can not perform this action (eg. students), so we return false for those users. If you do not implement this function then the event will always be visible.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Is the event visible?&lt;br /&gt;
 *&lt;br /&gt;
 * This is used to determine global visibility of an event in all places throughout Moodle. For example,&lt;br /&gt;
 * the ASSIGN_EVENT_TYPE_GRADINGDUE event will not be shown to students on their calendar, and&lt;br /&gt;
 * ASSIGN_EVENT_TYPE_DUE events will not be shown to teachers.&lt;br /&gt;
 *&lt;br /&gt;
 * @param calendar_event $event&lt;br /&gt;
 * @return bool Returns true if the event is visible to the current user, false otherwise.&lt;br /&gt;
 */&lt;br /&gt;
function mod_assign_core_calendar_is_event_visible(calendar_event $event) {&lt;br /&gt;
    global $CFG, $USER;&lt;br /&gt;
&lt;br /&gt;
    require_once($CFG-&amp;gt;dirroot . &#039;/mod/assign/locallib.php&#039;);&lt;br /&gt;
&lt;br /&gt;
    $cm = get_fast_modinfo($event-&amp;gt;courseid)-&amp;gt;instances[&#039;assign&#039;][$event-&amp;gt;instance];&lt;br /&gt;
    $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
    $assign = new assign($context, $cm, null);&lt;br /&gt;
&lt;br /&gt;
    if ($event-&amp;gt;eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {&lt;br /&gt;
        return $assign-&amp;gt;can_grade();&lt;br /&gt;
    } else {&lt;br /&gt;
        return !$assign-&amp;gt;can_grade() &amp;amp;&amp;amp; $assign-&amp;gt;can_view_submission($USER-&amp;gt;id);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====mod_xyz_core_calendar_provide_event_action()====&lt;br /&gt;
This function takes a calendar event and provides the action associated with it, or null if there is none in which case the event will not be shown in block_myoverview (but will still be shown in the calendar block). This is used by the block_myoverview plugin. If you do not implement this function then the events created by your plugin will not be shown on the block.&lt;br /&gt;
&lt;br /&gt;
Eg.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
function mod_scorm_core_calendar_provide_event_action(calendar_event $event,&lt;br /&gt;
        \core_calendar\action_factory $factory) {&lt;br /&gt;
    global $CFG;&lt;br /&gt;
&lt;br /&gt;
    require_once($CFG-&amp;gt;dirroot . &#039;/mod/scorm/locallib.php&#039;);&lt;br /&gt;
&lt;br /&gt;
    $cm = get_fast_modinfo($event-&amp;gt;courseid)-&amp;gt;instances[&#039;scorm&#039;][$event-&amp;gt;instance];&lt;br /&gt;
&lt;br /&gt;
    if (!empty($cm-&amp;gt;customdata[&#039;timeclose&#039;]) &amp;amp;&amp;amp; $cm-&amp;gt;customdata[&#039;timeclose&#039;] &amp;lt; time()) {&lt;br /&gt;
        // The scorm has closed so the user can no longer submit anything.&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen.&lt;br /&gt;
    $customdata = $cm-&amp;gt;customdata ?: [];&lt;br /&gt;
    $customdata[&#039;id&#039;] = $cm-&amp;gt;instance;&lt;br /&gt;
    $scorm = (object)($customdata + [&#039;timeclose&#039; =&amp;gt; 0, &#039;timeopen&#039; =&amp;gt; 0]);&lt;br /&gt;
&lt;br /&gt;
    // Check that the SCORM activity is open.&lt;br /&gt;
    list($actionable, $warnings) = scorm_get_availability_status($scorm);&lt;br /&gt;
&lt;br /&gt;
    return $factory-&amp;gt;create_instance(&lt;br /&gt;
        get_string(&#039;enter&#039;, &#039;scorm&#039;),&lt;br /&gt;
        new \moodle_url(&#039;/mod/scorm/view.php&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;id)),&lt;br /&gt;
        1,&lt;br /&gt;
        $actionable&lt;br /&gt;
    );&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
The variables to pass to &amp;lt;code&amp;gt;create_instance()&amp;lt;/code&amp;gt; are -&lt;br /&gt;
&lt;br /&gt;
# $name String The name of the event, eg. get_string(&#039;dosomething&#039;, &#039;mod_xyz&#039;).&lt;br /&gt;
# $url \moodle_url The URL the user visits in order to perform this action.&lt;br /&gt;
# $itemcount int This represents the number of items that require action (eg. Need to write 3 forum posts). If this is 0 then the event is not displayed.&lt;br /&gt;
# $actionable bool This determines if the event is currently able to be acted on. Eg. the activity may not currently be open due to date restrictions so the event is shown to the user to let them know that there is an upcoming event but the url will not be active.&lt;br /&gt;
&lt;br /&gt;
====mod_xyz_core_calendar_event_action_shows_item_count()====&lt;br /&gt;
This function determines if a given event should display the number of items to action on block_myoverview. For example, if the event type is ASSIGN_EVENT_TYPE_GRADINGDUE then we only display the item count if there are one or more assignments to grade. If you do not implement this function then the item count is always hidden. This is usually fine as the majority of events only have an item count of &#039;1&#039; (eg. Submitting an assignment) and there is no need display the item count.&lt;br /&gt;
&lt;br /&gt;
Eg.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Callback function that determines whether an action event should be showing its item count&lt;br /&gt;
 * based on the event type and the item count.&lt;br /&gt;
 *&lt;br /&gt;
 * @param calendar_event $event The calendar event.&lt;br /&gt;
 * @param int $itemcount The item count associated with the action event.&lt;br /&gt;
 * @return bool&lt;br /&gt;
 */&lt;br /&gt;
function mod_assign_core_calendar_event_action_shows_item_count(calendar_event $event, $itemcount = 0) {&lt;br /&gt;
    // List of event types where the action event&#039;s item count should be shown.&lt;br /&gt;
    $eventtypesshowingitemcount = [&lt;br /&gt;
        ASSIGN_EVENT_TYPE_GRADINGDUE&lt;br /&gt;
    ];&lt;br /&gt;
    // For mod_assign, item count should be shown if the event type is &#039;gradingdue&#039; and there is one or more item count.&lt;br /&gt;
    return in_array($event-&amp;gt;eventtype, $eventtypesshowingitemcount) &amp;amp;&amp;amp; $itemcount &amp;gt; 0;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Refreshing calendar events of activity modules==&lt;br /&gt;
A new ad-hoc task  &#039;refresh_mod_calendar_events_task&#039; has been created. This task basically loops through all of the activity modules that implement the &#039;*_refresh_events()&#039; hook.&lt;br /&gt;
&lt;br /&gt;
Sample usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// Create the instance.&lt;br /&gt;
$refreshtask = new refresh_mod_calendar_events_task();&lt;br /&gt;
&lt;br /&gt;
// Add custom data.&lt;br /&gt;
$customdata = [&lt;br /&gt;
    &#039;plugins&#039; =&amp;gt; [&#039;assign&#039;, &#039;lesson&#039;, &#039;quiz&#039;] // Optional. If not specified, it will refresh the events of all of the activity modules.&lt;br /&gt;
];&lt;br /&gt;
$refreshtask-&amp;gt;set_custom_data($customdata);&lt;br /&gt;
&lt;br /&gt;
// Queue it.&lt;br /&gt;
\core\task\manager::queue_adhoc_task($refreshtask);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==calendar_get_legacy_events()==&lt;br /&gt;
This functions accepts the same inputs as &#039;calendar_get_events()&#039; but is now utilising the new Moodle Calendar API system. It respects overrides and will also add the action properties, whenever appropriate.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note that this function will not work as expected if you pass a list of user ids as the current user session is internally used to determine which events should be visible. More info in https://tracker.moodle.org/browse/MDL-60340&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Changes to Behat==&lt;br /&gt;
The &amp;quot;And I follow &amp;quot;Course1&amp;quot;&amp;quot; Behat step won&#039;t work from the Dashboard anymore and has been replaced with &amp;quot;And I am on &amp;quot;Course 1&amp;quot; course homepage&amp;quot; where &#039;Course 1&#039; is the fullname of the course.&lt;br /&gt;
&lt;br /&gt;
==Drag &amp;amp; drop==&lt;br /&gt;
&lt;br /&gt;
The calendar supports dragging and dropping events within the calendar in order to change the start day for the event. Each type of calendar event can be dragged by a user with sufficient permissions to edit the event.&lt;br /&gt;
&lt;br /&gt;
===Dragging action events===&lt;br /&gt;
&lt;br /&gt;
When an action event is dragged the corresponding property will also be updated in the activity instance that generated the event. For example, dragging the assignment due date event will result in the assignment activity’s due date to be changed.&lt;br /&gt;
&lt;br /&gt;
In order to drag an action event the logged in user must have the moodle/course:manageactivities capability in the activity that generated the event.&lt;br /&gt;
&lt;br /&gt;
For an action event to be draggable the activity that generated it will need to have implemented at least one (but ideally both) callback to handle updating itself after the calendar event is dragged.&lt;br /&gt;
&lt;br /&gt;
====core_calendar_event_timestart_updated (required)====&lt;br /&gt;
This callback is required to be implemented by any activity that wishes to have it’s action events draggable in the calendar.&lt;br /&gt;
&lt;br /&gt;
This callback handles updating the activity instance based on the changed action event. The callback will receive the updated calendar event and the corresponding activity instance.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
function mod_feedback_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $feedback) {&lt;br /&gt;
    global $CFG, $DB;&lt;br /&gt;
&lt;br /&gt;
    if (empty($event-&amp;gt;instance) || $event-&amp;gt;modulename != &#039;feedback&#039;) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if ($event-&amp;gt;instance != $feedback-&amp;gt;id) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (!in_array($event-&amp;gt;eventtype, [FEEDBACK_EVENT_TYPE_OPEN, FEEDBACK_EVENT_TYPE_CLOSE])) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $courseid = $event-&amp;gt;courseid;&lt;br /&gt;
    $modulename = $event-&amp;gt;modulename;&lt;br /&gt;
    $instanceid = $event-&amp;gt;instance;&lt;br /&gt;
    $modified = false;&lt;br /&gt;
&lt;br /&gt;
    $coursemodule = get_fast_modinfo($courseid)-&amp;gt;instances[$modulename][$instanceid];&lt;br /&gt;
    $context = context_module::instance($coursemodule-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
    // The user does not have the capability to modify this activity.&lt;br /&gt;
    if (!has_capability(&#039;moodle/course:manageactivities&#039;, $context)) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if ($event-&amp;gt;eventtype == FEEDBACK_EVENT_TYPE_OPEN) {&lt;br /&gt;
        // If the event is for the feedback activity opening then we should&lt;br /&gt;
        // set the start time of the feedback activity to be the new start&lt;br /&gt;
        // time of the event.&lt;br /&gt;
        if ($feedback-&amp;gt;timeopen != $event-&amp;gt;timestart) {&lt;br /&gt;
            $feedback-&amp;gt;timeopen = $event-&amp;gt;timestart;&lt;br /&gt;
            $feedback-&amp;gt;timemodified = time();&lt;br /&gt;
            $modified = true;&lt;br /&gt;
        }&lt;br /&gt;
    } else if ($event-&amp;gt;eventtype == FEEDBACK_EVENT_TYPE_CLOSE) {&lt;br /&gt;
        // If the event is for the feedback activity closing then we should&lt;br /&gt;
        // set the end time of the feedback activity to be the new start&lt;br /&gt;
        // time of the event.&lt;br /&gt;
        if ($feedback-&amp;gt;timeclose != $event-&amp;gt;timestart) {&lt;br /&gt;
            $feedback-&amp;gt;timeclose = $event-&amp;gt;timestart;&lt;br /&gt;
            $modified = true;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if ($modified) {&lt;br /&gt;
        $feedback-&amp;gt;timemodified = time();&lt;br /&gt;
        $DB-&amp;gt;update_record(&#039;feedback&#039;, $feedback);&lt;br /&gt;
        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);&lt;br /&gt;
        $event-&amp;gt;trigger();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core_calendar_get_valid_event_timestart_range====&lt;br /&gt;
This callback should calculate the minimum and maximum allowed timestart property for the given calendar event. This will typically be based on the properties of the activity instance, for example the timeopen and timeclose properties of the activity could form the minimum and maximum bounds, respectively.&lt;br /&gt;
&lt;br /&gt;
These values will be used to provide a visual indicator to the user in the calendar UI for which days are valid for the event to be dragged to. It will also be used to validate that the calendar event is being updated to a valid timestart value.&lt;br /&gt;
&lt;br /&gt;
The callback should return an array with two values, the first value representing the minimum cutoff and the second the maximum.&lt;br /&gt;
&lt;br /&gt;
The callback can return an array for each of the minimum and maximum cutoffs, if it has them. The array should contain the timestamp of the cutoff and an error message to be displayed to the user if they attempt to drag an event to a day that violates the limit. For example:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
[&lt;br /&gt;
     [1505704373, &#039;The due date must be after the sbumission start date&#039;], // Minimum cutoff.&lt;br /&gt;
     [1506741172, &#039;The due date must be before the cutoff date&#039;] // Maximum cutoff.&lt;br /&gt;
]&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If the calendar event has no limits then it should return null in for either/both of the min and max cutoff values to indicate that it isn’t limited. For example:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
[null, null] // No limits.&lt;br /&gt;
&amp;lt;/code&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
 &amp;lt;code php&amp;gt;&lt;br /&gt;
[null, [1510625037, “This is the maximum cutoff”]] // No minimum cutoff.&lt;br /&gt;
&amp;lt;/code&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
[[1510625037, “This is the minimum cutoff”], null] // No maximum cutoff.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If the calendar event has no valid timestart values then the callback should return [false, false]. This is used to prevent the drag and drop of override events in activities that support them (e.g. Assign, Quiz).&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
function mod_feedback_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {&lt;br /&gt;
    $mindate = null;&lt;br /&gt;
    $maxdate = null;&lt;br /&gt;
&lt;br /&gt;
    if ($event-&amp;gt;eventtype == FEEDBACK_EVENT_TYPE_OPEN) {&lt;br /&gt;
        // The start time of the open event can&#039;t be equal to or after the&lt;br /&gt;
        // close time of the choice activity.&lt;br /&gt;
        if (!empty($instance-&amp;gt;timeclose)) {&lt;br /&gt;
            $maxdate = [&lt;br /&gt;
                $instance-&amp;gt;timeclose,&lt;br /&gt;
                get_string(&#039;openafterclose&#039;, &#039;feedback&#039;)&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
    } else if ($event-&amp;gt;eventtype == FEEDBACK_EVENT_TYPE_CLOSE) {&lt;br /&gt;
        // The start time of the close event can&#039;t be equal to or earlier than the&lt;br /&gt;
        // open time of the choice activity.&lt;br /&gt;
        if (!empty($instance-&amp;gt;timeopen)) {&lt;br /&gt;
            $mindate = [&lt;br /&gt;
                $instance-&amp;gt;timeopen,&lt;br /&gt;
                get_string(&#039;closebeforeopen&#039;, &#039;feedback&#039;)&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return [$mindate, $maxdate];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_Moodle_Mobile_2_(Ionic_1)&amp;diff=51884</id>
		<title>Setting up your development environment for Moodle Mobile 2 (Ionic 1)</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_Moodle_Mobile_2_(Ionic_1)&amp;diff=51884"/>
		<updated>2017-02-15T10:48:26Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* node-gyp\src\win_delay_load_hook.c(34): error C2373: &amp;#039;__pfnDliNotifyHook2&amp;#039;: redefinition; different type modifiers */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Work in progress}}&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The majority of your development work will be done using the browser. You will likely begin to use an emulator once you need to simulate a real mobile device.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install a browser for development&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
I recommend Chromium browser (Google Chrome open source version) https://download-chromium.appspot.com/&lt;br /&gt;
Please, read Moodle_Mobile_development_using_Chrome_or_Chromium for more information&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install Node.js&#039;&#039;&#039; &lt;br /&gt;
&lt;br /&gt;
http://nodejs.org for Mac users I recommend to install nodejs via macports.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;IMPORTANT&#039;&#039;&#039;: Don&#039;t use node 4.0 or higher, there are incompatibilities with some of the packages we use. Please use v0.12.7 instead. The download directory for v0.12.7 is [https://nodejs.org/dist/v0.12.7/ here]&lt;br /&gt;
&lt;br /&gt;
&amp;quot;note&amp;quot;: if you also need later versions of node for other projects, use Node Version Manager aka &amp;quot;nvm&amp;quot; [https://github.com/creationix/nvm here]. Linux and OSX is supported, and the site has pointers to windows alternatives. Simply type:&lt;br /&gt;
   nvm install 0.12.15&lt;br /&gt;
   nvm use 0.12.15&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install ionic:&#039;&#039;&#039; &lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install the npm required packages&#039;&#039;&#039;&lt;br /&gt;
 sudo npm install -g bower (This will install bower in a folder that should be in the PATH)&lt;br /&gt;
 sudo npm install -g gulp (This will install gulp in a folder that should be in the PATH)&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Clone the code base into a local directory in your computer.&#039;&#039;&#039;&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install the npm plugins dependencies&#039;&#039;&#039;&lt;br /&gt;
 npm install (This will install all the dependencies listed in package.json)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Add the iOS and Android platforms and install the required Cordova plugins&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Install the platforms. It&#039;s important to use these versions because newer versions are going to cause some problems with the plugins installed.&lt;br /&gt;
 ionic platform add android@5.1.1&lt;br /&gt;
 ionic platform add ios@4.1.0&lt;br /&gt;
&lt;br /&gt;
Run the following command to install the platforms and all the required Cordova plugins&lt;br /&gt;
 ionic state restore&lt;br /&gt;
&lt;br /&gt;
Please, note that if you are creating a custom app with a custom URL scheme, you should edit the /package.json file and specify there your custom URL_SCHEME (replacing the existing value) and your [https://github.com/phonegap/phonegap-plugin-push/blob/master/docs/INSTALLATION.md GCMPN SENDER_ID].&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install bower globally and the required javascript files&#039;&#039;&#039;&lt;br /&gt;
 bower install (this will install all the libraries listed in bower.json)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Run gulp’s default tasks (in order to create the build files)&#039;&#039;&#039;&lt;br /&gt;
 gulp &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open the app in the browser&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
First start chromium via the command line using the custom parameters as is mentioned here: [[Moodle Mobile development using Chrome or Chromium]]&lt;br /&gt;
&lt;br /&gt;
and then, start the Ionic server:&lt;br /&gt;
&lt;br /&gt;
 ionic serve --browser chromium&lt;br /&gt;
&lt;br /&gt;
== Editor == &lt;br /&gt;
&lt;br /&gt;
There are several plugins for AngularJS and Ionic development available for common editors.&lt;br /&gt;
&lt;br /&gt;
Sublime 3 has plugins for [https://github.com/SublimeLinter/SublimeLinter-jscs jscs] (coding style) and [https://github.com/SublimeLinter/SublimeLinter-jshint jshint] (linter)&lt;br /&gt;
&lt;br /&gt;
Configuration files for jscs and jshint can be downloaded from https://github.com/angular/angular.js (.jscs.json and .jshint*)&lt;br /&gt;
&lt;br /&gt;
== Updating ionic and cordova ==&lt;br /&gt;
&lt;br /&gt;
 sudo npm update -g cordova&lt;br /&gt;
 sudo npm update -g ionic&lt;br /&gt;
&lt;br /&gt;
 ionic platform update android&lt;br /&gt;
 ionic platform update ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin rm your_plugin_id&lt;br /&gt;
 cordova plugin add your_plugin_id&lt;br /&gt;
&lt;br /&gt;
== Building for Android and iOS ==&lt;br /&gt;
&lt;br /&gt;
Please see this guide to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
http://cordova.apache.org/docs/en/5.0.0/guide_platforms_index.md.html#Platform%20Guides&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Common errors when building&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* com.android.dex.DexException: Multiple dex files define XXX&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add these code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations {&lt;br /&gt;
      all*.exclude group: &#039;com.android.support&#039;, module: &#039;support-v4&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
* Could not resolve all dependencies for configuration &#039;:_debugCompile&#039;.&lt;br /&gt;
Open the Android SDK Manager and make sure you have installed: Android Support Repository, Android Support Library, Google Play Services and Google Repository.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Error: libsass bindings not found. Try reinstalling node-sass? ===&lt;br /&gt;
&lt;br /&gt;
Please read: http://fettblog.eu/gulp-and-node4-first-aid/, alternatively you must be sure that you installed Node v0.12&lt;br /&gt;
&lt;br /&gt;
=== node-gyp\src\win_delay_load_hook.c(34): error C2373: &#039;__pfnDliNotifyHook2&#039;: redefinition; different type modifiers ===&lt;br /&gt;
&lt;br /&gt;
Try updating npm to the latest version using: &lt;br /&gt;
&lt;br /&gt;
  npm install -g npm@latest&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
http://ionicframework.com/docs/cli/&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_Moodle_Mobile_2_(Ionic_1)&amp;diff=51883</id>
		<title>Setting up your development environment for Moodle Mobile 2 (Ionic 1)</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_Moodle_Mobile_2_(Ionic_1)&amp;diff=51883"/>
		<updated>2017-02-15T10:47:14Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Troubleshooting */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Work in progress}}&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
The majority of your development work will be done using the browser. You will likely begin to use an emulator once you need to simulate a real mobile device.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install a browser for development&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
I recommend Chromium browser (Google Chrome open source version) https://download-chromium.appspot.com/&lt;br /&gt;
Please, read Moodle_Mobile_development_using_Chrome_or_Chromium for more information&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install Node.js&#039;&#039;&#039; &lt;br /&gt;
&lt;br /&gt;
http://nodejs.org for Mac users I recommend to install nodejs via macports.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;IMPORTANT&#039;&#039;&#039;: Don&#039;t use node 4.0 or higher, there are incompatibilities with some of the packages we use. Please use v0.12.7 instead. The download directory for v0.12.7 is [https://nodejs.org/dist/v0.12.7/ here]&lt;br /&gt;
&lt;br /&gt;
&amp;quot;note&amp;quot;: if you also need later versions of node for other projects, use Node Version Manager aka &amp;quot;nvm&amp;quot; [https://github.com/creationix/nvm here]. Linux and OSX is supported, and the site has pointers to windows alternatives. Simply type:&lt;br /&gt;
   nvm install 0.12.15&lt;br /&gt;
   nvm use 0.12.15&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install ionic:&#039;&#039;&#039; &lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install the npm required packages&#039;&#039;&#039;&lt;br /&gt;
 sudo npm install -g bower (This will install bower in a folder that should be in the PATH)&lt;br /&gt;
 sudo npm install -g gulp (This will install gulp in a folder that should be in the PATH)&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Clone the code base into a local directory in your computer.&#039;&#039;&#039;&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install the npm plugins dependencies&#039;&#039;&#039;&lt;br /&gt;
 npm install (This will install all the dependencies listed in package.json)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Add the iOS and Android platforms and install the required Cordova plugins&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Install the platforms. It&#039;s important to use these versions because newer versions are going to cause some problems with the plugins installed.&lt;br /&gt;
 ionic platform add android@5.1.1&lt;br /&gt;
 ionic platform add ios@4.1.0&lt;br /&gt;
&lt;br /&gt;
Run the following command to install the platforms and all the required Cordova plugins&lt;br /&gt;
 ionic state restore&lt;br /&gt;
&lt;br /&gt;
Please, note that if you are creating a custom app with a custom URL scheme, you should edit the /package.json file and specify there your custom URL_SCHEME (replacing the existing value) and your [https://github.com/phonegap/phonegap-plugin-push/blob/master/docs/INSTALLATION.md GCMPN SENDER_ID].&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install bower globally and the required javascript files&#039;&#039;&#039;&lt;br /&gt;
 bower install (this will install all the libraries listed in bower.json)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Run gulp’s default tasks (in order to create the build files)&#039;&#039;&#039;&lt;br /&gt;
 gulp &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open the app in the browser&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
First start chromium via the command line using the custom parameters as is mentioned here: [[Moodle Mobile development using Chrome or Chromium]]&lt;br /&gt;
&lt;br /&gt;
and then, start the Ionic server:&lt;br /&gt;
&lt;br /&gt;
 ionic serve --browser chromium&lt;br /&gt;
&lt;br /&gt;
== Editor == &lt;br /&gt;
&lt;br /&gt;
There are several plugins for AngularJS and Ionic development available for common editors.&lt;br /&gt;
&lt;br /&gt;
Sublime 3 has plugins for [https://github.com/SublimeLinter/SublimeLinter-jscs jscs] (coding style) and [https://github.com/SublimeLinter/SublimeLinter-jshint jshint] (linter)&lt;br /&gt;
&lt;br /&gt;
Configuration files for jscs and jshint can be downloaded from https://github.com/angular/angular.js (.jscs.json and .jshint*)&lt;br /&gt;
&lt;br /&gt;
== Updating ionic and cordova ==&lt;br /&gt;
&lt;br /&gt;
 sudo npm update -g cordova&lt;br /&gt;
 sudo npm update -g ionic&lt;br /&gt;
&lt;br /&gt;
 ionic platform update android&lt;br /&gt;
 ionic platform update ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin rm your_plugin_id&lt;br /&gt;
 cordova plugin add your_plugin_id&lt;br /&gt;
&lt;br /&gt;
== Building for Android and iOS ==&lt;br /&gt;
&lt;br /&gt;
Please see this guide to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
http://cordova.apache.org/docs/en/5.0.0/guide_platforms_index.md.html#Platform%20Guides&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Common errors when building&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* com.android.dex.DexException: Multiple dex files define XXX&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add these code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations {&lt;br /&gt;
      all*.exclude group: &#039;com.android.support&#039;, module: &#039;support-v4&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
* Could not resolve all dependencies for configuration &#039;:_debugCompile&#039;.&lt;br /&gt;
Open the Android SDK Manager and make sure you have installed: Android Support Repository, Android Support Library, Google Play Services and Google Repository.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Error: libsass bindings not found. Try reinstalling node-sass? ===&lt;br /&gt;
&lt;br /&gt;
Please read: http://fettblog.eu/gulp-and-node4-first-aid/, alternatively you must be sure that you installed Node v0.12&lt;br /&gt;
&lt;br /&gt;
=== node-gyp\src\win_delay_load_hook.c(34): error C2373: &#039;__pfnDliNotifyHook2&#039;: redefinition; different type modifiers ===&lt;br /&gt;
&lt;br /&gt;
Try updating npm to the latest version using: npm install -g npm@latest&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
http://ionicframework.com/docs/cli/&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Cache_API&amp;diff=46493</id>
		<title>Cache API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Cache_API&amp;diff=46493"/>
		<updated>2014-10-16T14:34:08Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Specifying a data source */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This document details the Cache API aka MUC aka Moodle&#039;s Universal Cache.&lt;br /&gt;
I&#039;ve chosen to use a tutorial/example flow for this document always working with a theoretical module plugin called myplugin.&lt;br /&gt;
There is also a [[Cache API - Quick reference]] if you would rather read that.&lt;br /&gt;
&lt;br /&gt;
==Basic usage==&lt;br /&gt;
It&#039;s very easy to get started with the Cache API. It is designed to be as easy and as quick to use as possible and is predominantely self contained.&lt;br /&gt;
All you need to do is add a definition for your cache and you are ready to start working with the Cache API.&lt;br /&gt;
&lt;br /&gt;
===Creating a definition===&lt;br /&gt;
Cache definitions exist within the db/caches.php file for a component/plugin.&amp;lt;br /&amp;gt;&lt;br /&gt;
In the case of core that is the moodle/lib/db/caches.php file, in the case of a module that would be moodle/mod/myplugin/db/caches.php.&lt;br /&gt;
&lt;br /&gt;
The definition is used API in order to understand a little about the cache and what it is being used for, it also allows the administrator to set things up especially for the definition if they want.&lt;br /&gt;
From a development point of view the definition allows you to tell the API about your cache, what it requires, and any (if any) advanced features you want it to have.&amp;lt;br /&amp;gt;&lt;br /&gt;
The following shows a basic definition containing just the bare minimum:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// moodle/mod/myplugin/db/caches.php&lt;br /&gt;
$definitions = array(&lt;br /&gt;
    &#039;somedata&#039; =&amp;gt; array(&lt;br /&gt;
        &#039;mode&#039; =&amp;gt; cache_store::MODE_APPLICATION&lt;br /&gt;
    )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
This informs the API that the myplugin module has a cache called &#039;&#039;somedata&#039;&#039; and that it is an application (globally shared) cache.&amp;lt;br /&amp;gt;&lt;br /&gt;
When creating a definition thats the bare minimum, to provide an area &#039;&#039;(somedata)&#039;&#039; and declare the type of the cache application, session, or request.&amp;lt;br /&amp;gt;&lt;br /&gt;
:An application cache is a shared cache, all users can access it.&amp;lt;br /&amp;gt;&lt;br /&gt;
:Session caches are, well, just stores in the users session.&amp;lt;br /&amp;gt;&lt;br /&gt;
:Request caches you can think of as static caches, only available to the user owning the request, and only alive until the end of the request.&amp;lt;br /&amp;gt;&lt;br /&gt;
There are of course many more options available that allow you to really take the cache by the reigns, you can read about some of the important ones further on, or skip ahead to [[#The definition]] section which details the available options in full.&lt;br /&gt;
&lt;br /&gt;
Please note that for each definition a language string with the name &#039;&#039;cachedef_&#039;&#039; followed by the name of the definition is expected. Using the example above you would have to define:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// moodle/mod/myplugin/lang/en/mod_myplugin.php&lt;br /&gt;
$string[&#039;cachedef_somedata&#039;] = &#039;This is the description of the cache somedata&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Getting a cache object===&lt;br /&gt;
Once your definition has been created you should bump the version number so that Moodle upgrades and processes the definitions file at which point your definition will be useable.&lt;br /&gt;
&lt;br /&gt;
Now within code you can get a cache object to interact with in the following manner.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$cache = cache::make(&#039;mod_myplugin&#039;, &#039;mycache&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The cache::make method is a factory method, it will create a cache object to allow you to work with your cache. The cache object will be one of several classes chosen by the API based upon what your definition contains. All of these classes will extend the base cache class, and in nearly all cases you will get one of cache_application, cache_session, or cache_request depending upon the mode you selected.&lt;br /&gt;
&lt;br /&gt;
===Using your cache object===&lt;br /&gt;
Once you have a cache object (will extend the cache class and implements cache_loader) you are ready to start interacting with the cache.&lt;br /&gt;
&lt;br /&gt;
Of course there are three basic basic operations. get, set, and delete.&lt;br /&gt;
&lt;br /&gt;
The first is to send something to the cache.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $cache-&amp;gt;set(&#039;key&#039;, &#039;value&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Easy enough. The key must an int or a string. The value can be absolutely anything your want.&lt;br /&gt;
The result is true if the operation was a success, false otherwise.&lt;br /&gt;
&lt;br /&gt;
The second is to fetch something from the cache.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$data = $cache-&amp;gt;get(&#039;key&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
$data will either be what ever was being stored in the cache, or false if the cache could not find the key.&lt;br /&gt;
&lt;br /&gt;
The third and final operation is delete.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $cache-&amp;gt;delete(&#039;key&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Again just like set the result will either be true if the operation was a success, or false otherwise.&lt;br /&gt;
&lt;br /&gt;
You can also set, get, and delete multiple key=&amp;gt;value pairs in a single transaction.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $cache-&amp;gt;set_many(array(&lt;br /&gt;
    &#039;key1&#039; =&amp;gt; &#039;data1&#039;,&lt;br /&gt;
    &#039;key3&#039; =&amp;gt; &#039;data3&#039;&lt;br /&gt;
));&lt;br /&gt;
// $result will be the number of pairs sucessfully set.&lt;br /&gt;
&lt;br /&gt;
$result = $cache-&amp;gt;get_many(array(&#039;key1&#039;, &#039;key2&#039;, &#039;key3&#039;));&lt;br /&gt;
print_r($result);&lt;br /&gt;
// Will print the following:&lt;br /&gt;
// array(&lt;br /&gt;
//     &#039;key1&#039; =&amp;gt; &#039;data1&#039;,&lt;br /&gt;
//     &#039;key2&#039; =&amp;gt; false,&lt;br /&gt;
//     &#039;key3&#039; =&amp;gt; &#039;data3&#039;&lt;br /&gt;
// )&lt;br /&gt;
&lt;br /&gt;
$result = $cache-&amp;gt;delete_many(array(&#039;key1&#039;, &#039;key3&#039;);&lt;br /&gt;
// $result will be the number of records sucessfully deleted.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That covers the basic operation of the Cache API.&amp;lt;br /&amp;gt;&lt;br /&gt;
In many situations there is not going to be any more to it than that.&lt;br /&gt;
&lt;br /&gt;
==Ad-hoc Caches==&lt;br /&gt;
This is the alternative method of using the cache API.&amp;lt;br /&amp;gt;&lt;br /&gt;
It involves creating a cache using just the required params at the time that it is required. It doesn&#039;t require that a definition exists making it quicker and easier to use, however it can only use the default settings and is only recommended for insignificant caches (rarely used during operation, never to be mapped or customised, only existsing in a single place in code).&lt;br /&gt;
&lt;br /&gt;
Once a cache object has been retrieved it operates exactly as the same as a cache that has been created for a definition.&lt;br /&gt;
&lt;br /&gt;
To create an ad-hoc cache you would use the following:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$cache = cache::make_from_params(cache_store::MODE_APPLICATION, &#039;mod_myplugin&#039;, &#039;mycache&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Really don&#039;t be lazy, if you don&#039;t have a good reason to use an ad-hoc cache you should be spending a extra 5 minutes creating a definition.&lt;br /&gt;
&lt;br /&gt;
==The definition==&lt;br /&gt;
&lt;br /&gt;
The above section illustrated how to create a basic definition, specifying just the area name (the key) and the mode for the definition. Those being the two required properties for a definition.&amp;lt;br /&amp;gt;&lt;br /&gt;
There are many other options that will let you make the most of the Cache API and will undoubtedly be required when implementing and converting cache solutions to the Cache API.&lt;br /&gt;
&lt;br /&gt;
The following details the options available to a definition and their defaults if not applied:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$definitions = array(&lt;br /&gt;
    // The name of the cache area is the key. The component/plugin will be picked up from the file location.&lt;br /&gt;
    &#039;area&#039; =&amp;gt; array(&lt;br /&gt;
        &#039;mode&#039; =&amp;gt; cache_store::MODE_*,&lt;br /&gt;
        &#039;simplekeys&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;simpledata&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;requireidentifiers&#039; =&amp;gt; array(&#039;ident1&#039;, &#039;ident2&#039;),&lt;br /&gt;
        &#039;requiredataguarantee&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;requiremultipleidentifiers&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;requirelockingread&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;requirelockingwrite&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;maxsize&#039; =&amp;gt; null,&lt;br /&gt;
        &#039;overrideclass&#039; =&amp;gt; null,&lt;br /&gt;
        &#039;overrideclassfile&#039; =&amp;gt; null,&lt;br /&gt;
        &#039;datasource&#039; =&amp;gt; null,&lt;br /&gt;
        &#039;datasourcefile&#039; =&amp;gt; null,&lt;br /&gt;
        &#039;staticacceleration&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;staticaccelerationsize&#039; =&amp;gt; null,&lt;br /&gt;
        &#039;ttl&#039; =&amp;gt; 0,&lt;br /&gt;
        &#039;mappingsonly&#039; =&amp;gt; false,&lt;br /&gt;
        &#039;invalidationevents&#039; =&amp;gt; array(&#039;event1&#039;, &#039;event2&#039;),&lt;br /&gt;
        &#039;sharingoptions&#039; =&amp;gt; cache_definition::SHARING_DEFAULT,&lt;br /&gt;
        &#039;defaultsharing&#039; =&amp;gt; cache_definition::SHARING_DEFAULT,&lt;br /&gt;
    )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Setting requirements===&lt;br /&gt;
The definition can specify several requirements for the cache.&amp;lt;br /&amp;gt;&lt;br /&gt;
This includes identifiers that must be provided when creating the cache object, that the store guarantees data stored in it will remain there until removed, a store that supports multiple identifiers, and finally read/write locking.&lt;br /&gt;
The options for these are as follows:&lt;br /&gt;
&lt;br /&gt;
; simplekeys : [bool] Set to true if your cache will only use simple keys for its items.&amp;lt;br /&amp;gt;Simple keys consist of digits, underscores and the 26 chars of the english language. a-zA-Z0-9_&amp;lt;br /&amp;gt;If true the keys won&#039;t be hashed before being passed to the cache store for gets/sets/deletes. It will be better for performance and possible only because we know the keys are safe.&lt;br /&gt;
; simpledata : [bool] If set to true we know that the data is scalar or array of scalar.&lt;br /&gt;
; requireidentifiers : [array] An array of identifiers that must be provided to the cache when it is created.&lt;br /&gt;
; requiredataguarantee : [bool] If set to true then only stores that can guarantee data will remain available once set will be used.&lt;br /&gt;
; requiremultipleidentifiers : [bool] If set to true then only stores that support multiple identifiers will be used.&lt;br /&gt;
; requirelockingread : [bool] If set to true then a lock will be gained before reading from the cache store. It is recommended not to use this setting unless 100% absolutely positively required.&amp;lt;br /&amp;gt;Remember 99.9% of caches will NOT need this setting.&amp;lt;br /&amp;gt;This setting will only be used for application caches presently.&lt;br /&gt;
; requirelockingwrite : [bool] If set to true then a lock will be gained before writing to the cache store. As above this is not recommended unless truly needed. Please think about the order of your code and deal with race conditions there first.&amp;lt;br /&amp;gt;This setting will only be used for application caches presently.&lt;br /&gt;
&lt;br /&gt;
===Cache modifiers===&lt;br /&gt;
You are also to modify the way in which the cache is going to operate when working for your definition.&amp;lt;br /&amp;gt;&lt;br /&gt;
By enabling the static option the Cache API will only ever generate a single cache object for your definition on the first request for it, further requests will be returned the original instance.&amp;lt;br /&amp;gt;This greatly speeds up the collecting of a cache object.&amp;lt;br /&amp;gt;&lt;br /&gt;
Enabling persistence also enables a static store within the cache object, anything set to the cache, or retrieved from it will be stored in that static array for the life of the request.&lt;br /&gt;
This makes the persistence options some of the most powerful. If you know you are going to be using you cache over and over again or if you know you will be making lots of requests for the same items then this will provide a great performance boost.&lt;br /&gt;
Of course the static storage of cache objects and of data is costly in terms of memory and should only be used when actually required, as such it is turned off by default.&lt;br /&gt;
As well as persistence you can also set a maximum number of items that the cache should store (not a hard limit, its up to each store) and a time to live (ttl) although both are discouraged as efficient design negates the need for both in most situations.&lt;br /&gt;
&lt;br /&gt;
; staticacceleration : [bool] This setting does two important things. First it tells the cache API to only instantiate the cache structure for this definition once, further requests will be given the original instance.&amp;lt;br /&amp;gt;Second the cache loader will keep an array of the items set and retrieved to the cache during the request.&amp;lt;br /&amp;gt;This has several advantages including better performance without needing to start passing the cache instance between function calls, the downside is that the cache instance + the items used stay within memory.&amp;lt;br /&amp;gt;Consider using this setting when you know that there are going to be many calls to the cache for the same information or when you are converting existing code to the cache and need to access the cache within functions but don&#039;t want to add it as an argument to the function.&lt;br /&gt;
; staticaccelerationsize : [int] This supplements the above setting by limiting the number of items in the caches persistent array of items.&amp;lt;br /&amp;gt;Tweaking this setting lower will allow you to minimise the memory implications above while hopefully still managing to offset calls to the cache store.&lt;br /&gt;
; ttl : [int] A time to live for the data (in seconds). It is strongly recommended that you don&#039;t make use of this and instead try to create an event driven invalidation system (even if the event is just time expiring, better not to rely on ttl).&amp;lt;br /&amp;gt;Not all cache stores will support this natively and there are undesired performance impacts if the cache store does not.&lt;br /&gt;
; maxsize : [int] If set this will be used as the maximum number of entries within the cache store for this definition.&amp;lt;br /&amp;gt;Its important to note that cache stores don&#039;t actually have to acknowledge this setting or maintain it as a hard limit.&lt;br /&gt;
&lt;br /&gt;
===Overriding a cache loader===&lt;br /&gt;
This is a super advanced feature and should not be done. ever. Unless you have an very good reason to do so.&lt;br /&gt;
It allows you to create your own cache loader and have it be used instead of the default cache loader class. The cache object you get back from the make operations will be an instance of this class.&lt;br /&gt;
&lt;br /&gt;
; overrideclass : [string] A class to use as the loader for this cache. This is an advanced setting and will allow the developer of the definition to take 100% control of the caching solution.&amp;lt;br /&amp;gt;Any class used here must inherit the cache_loader interface and must extend default cache loader for the mode they are using.&lt;br /&gt;
; overrideclassfile : [string] Suplements the above setting indicated the file containing the class to be used. This file is included when required.&lt;br /&gt;
&lt;br /&gt;
===Specifying a data source===&lt;br /&gt;
This is a great wee feature, especially if your code is object orientated.&amp;lt;br /&amp;gt;&lt;br /&gt;
It allows you to specify a class that must inherit the cache_data_source object and will be used to load any information requested from the cache that is not already being stored.&amp;lt;br /&amp;gt;&lt;br /&gt;
When the requested key cannot be found in the cache the data source will be asked to load it. The data source will then return the information to the cache, the cache will store it, and it will then return it to the user as a request of their get request. Essentially no get request should ever fail if you have a data source specified.&lt;br /&gt;
&lt;br /&gt;
; datasource : [string] A class to use as the data loader for this definition.&amp;lt;br /&amp;gt;Any class used here must inherit the cache_data_source interface.&lt;br /&gt;
; datasourcefile : [string] Suplements the above setting indicated the file containing the class to be used. This file is included when required.&lt;br /&gt;
&lt;br /&gt;
===Misc settings===&lt;br /&gt;
The following are stand along settings that don&#039;t fall into any of the above categories.&lt;br /&gt;
&lt;br /&gt;
; invalidationevents : [array] An array of events that should cause this cache to invalidate some or all of the items within it.&lt;br /&gt;
; mappingsonly : [bool] If set to true only the mapped cache store(s) will be used and the default mode store will not. This is a super advanced setting and should not be used unless absolutely required. It allows you to avoid the default stores for one reason or another.&lt;br /&gt;
; sharingoptions : [int] The sharing options that are appropriate for this definition. Should be the sum of the possible options.&lt;br /&gt;
; defaultsharing : [int] The default sharing option to use. It&#039;s highly recommended that you don&#039;t set this unless there is a very specific reason not to use the system default.&lt;br /&gt;
&lt;br /&gt;
==The loader==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Using a data source==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Developing cache plugins==&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=PHPUnit&amp;diff=43535</id>
		<title>PHPUnit</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=PHPUnit&amp;diff=43535"/>
		<updated>2014-01-15T10:47:06Z</updated>

		<summary type="html">&lt;p&gt;Nmagill: /* Installation of PHPUnit via Composer */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 2.3}}&lt;br /&gt;
&lt;br /&gt;
=What is PHPUnit=&lt;br /&gt;
PHPUnit by Sebastian Bergmann is an advanced unit testing framework for PHP. It is installed as Composer dependency and is not part of Moodle installation. To run PHPUnit tests, you have to manually install it on your development computer or test server.&lt;br /&gt;
&lt;br /&gt;
Read the excellent guide at&lt;br /&gt;
* [http://phpunit.de/manual/current/en/index.html PHPUnit Manual]&lt;br /&gt;
&lt;br /&gt;
=Installation of PHPUnit via Composer=&lt;br /&gt;
&lt;br /&gt;
The installation procedure depends on your operating system.&lt;br /&gt;
* Check that /your/moodle/dirroot/composer.json exists and the contents are similar to&lt;br /&gt;
 {&lt;br /&gt;
     &amp;quot;require-dev&amp;quot;: {&lt;br /&gt;
         &amp;quot;phpunit/phpunit&amp;quot;: &amp;quot;3.7.*&amp;quot;,&lt;br /&gt;
         &amp;quot;phpunit/dbUnit&amp;quot;: &amp;quot;1.2.*&amp;quot;&lt;br /&gt;
     }&lt;br /&gt;
 }&lt;br /&gt;
* Install Composer dependency manager&lt;br /&gt;
&lt;br /&gt;
 cd /your/moodle/dirroot&lt;br /&gt;
 curl -s https://getcomposer.org/installer | php&lt;br /&gt;
&lt;br /&gt;
On Windows go to https://getcomposer.org/download/ and download the Composer-Setup.exe file&lt;br /&gt;
&lt;br /&gt;
* Execute Composer installer&lt;br /&gt;
&lt;br /&gt;
 cd /your/moodle/dirroot&lt;br /&gt;
 php composer.phar install --dev&lt;br /&gt;
&lt;br /&gt;
(If that gives you connection problems try...)&lt;br /&gt;
&lt;br /&gt;
 php composer.phar install --dev --prefer-source&lt;br /&gt;
&lt;br /&gt;
On Windows if you are behind a proxy you will need to setup an environment variable called HTTP_PROXY with a value detailing your HTTP Proxy address and port before composer will correctly download files.&lt;br /&gt;
&lt;br /&gt;
Detailed instructions:&lt;br /&gt;
* [http://getcomposer.org/doc/00-intro.md Composer documentation]&lt;br /&gt;
&lt;br /&gt;
Detailed instructions:&lt;br /&gt;
* [[PHPUnit installation in Windows]]&lt;br /&gt;
* [[PHPUnit installation in OS X]]&lt;br /&gt;
&lt;br /&gt;
=Initialisation of test environment=&lt;br /&gt;
&lt;br /&gt;
Our PHPUnit integration requires a dedicated database and dataroot.  First, add a new dataroot directory and prefix into your config.php, you can find examples in config-dist.php (scroll down to &#039;Section 9&#039;).&lt;br /&gt;
&lt;br /&gt;
 $CFG-&amp;gt;phpunit_prefix = &#039;phpu_&#039;;&lt;br /&gt;
 $CFG-&amp;gt;phpunit_dataroot = &#039;/home/example/phpu_moodledata&#039;;&lt;br /&gt;
&lt;br /&gt;
If your system does not have a direct connection to the Internet, you also need to specify your proxy in config.php - even though you normally specify it by using the admin settings user interface. (If you do not use a proxy, you can skip this step.) Check the settings on the relevant admin settings page, or from the mdl_config table in your database, if you are unsure of the correct values.&lt;br /&gt;
 &lt;br /&gt;
 // Normal proxy settings&lt;br /&gt;
 $CFG-&amp;gt;proxyhost = &#039;wwwcache.example.org&#039;;&lt;br /&gt;
 $CFG-&amp;gt;proxyport = 80;&lt;br /&gt;
 $CFG-&amp;gt;proxytype = &#039;HTTP&#039;;&lt;br /&gt;
 $CFG-&amp;gt;proxybypass = &#039;localhost, 127.0.0.1, .example.org&#039;;&lt;br /&gt;
 // Omit the next lines if your proxy doesn&#039;t need a username/password:&lt;br /&gt;
 $CFG-&amp;gt;proxyuser = &#039;systemusername&#039;;&lt;br /&gt;
 $CFG-&amp;gt;proxypassword = &#039;systempassword&#039;;&lt;br /&gt;
&lt;br /&gt;
Then you need to initialise the test environment using following command.&lt;br /&gt;
&lt;br /&gt;
 cd /home/example/moodle&lt;br /&gt;
 php admin/tool/phpunit/cli/init.php&lt;br /&gt;
&lt;br /&gt;
This command has to be repeated after any upgrade, plugin (un)installation or if you have added tests to a plugin you are developing:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;NOTE:&#039;&#039;&#039; make sure that your php cli executable (or the one you want to use) is correctly on your path as the individual init scripts will call it repeatedly.&lt;br /&gt;
&lt;br /&gt;
=Test execution=&lt;br /&gt;
&lt;br /&gt;
To execute all test suites from main configuration file execute the &amp;lt;code&amp;gt;vendor/bin/phpunit&amp;lt;/code&amp;gt; script from your &amp;lt;code&amp;gt;$CFG-&amp;gt;dirroot&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
 cd /home/example/moodle&lt;br /&gt;
 vendor/bin/phpunit&lt;br /&gt;
&lt;br /&gt;
The rest of examples uses &amp;lt;code&amp;gt;phpunit&amp;lt;/code&amp;gt;, please substitute it with &amp;lt;code&amp;gt;vendor/bin/phpunit&amp;lt;/code&amp;gt; or create a shortcut in your dirroot.&lt;br /&gt;
&lt;br /&gt;
In IDEs, you may need to specify the path to the PHPUnit configuration file. Use the absolute path to &amp;lt;code&amp;gt;phpunit.xml&amp;lt;/code&amp;gt; from your &amp;lt;code&amp;gt;$CFG-&amp;gt;dirroot&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
There is an alternative script for running of tests via web interface: &amp;lt;code&amp;gt;admin/tool/phpunit/webrunner.php&amp;lt;/code&amp;gt;. Use this as the last resort only when you cannot use the command-line interface on your test server. It will most probably break due to permissions problems if you try to execute it both from command-line and from webrunner. This feature is not officially supported.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===How to run only some tests===&lt;br /&gt;
&lt;br /&gt;
==== Running a single test quickly ====&lt;br /&gt;
&lt;br /&gt;
The fastest way to run a single test is:&lt;br /&gt;
&lt;br /&gt;
 vendor/bin/phpunit my_test_class_name my/tests/filename.php&lt;br /&gt;
&lt;br /&gt;
This is faster than other methods because it avoids the need to search for the test file, but you should be careful because it may be possible to run tests this way which are not included in the normal run. If you use this method, do at least one full test run (or --group run, as below) to ensure the test can be found.&lt;br /&gt;
&lt;br /&gt;
==== Using the @group annotation ====&lt;br /&gt;
&lt;br /&gt;
If you add annotations like&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Unit tests for {@link stack_cas_keyval}.&lt;br /&gt;
 * @group qtype_stack&lt;br /&gt;
 */&lt;br /&gt;
class stack_cas_keyval_exception_test extends basic_testcase {&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
to all the classes in your plugin, the you can run just the tests for your plugin by doing&lt;br /&gt;
&lt;br /&gt;
 phpunit --group qtype_stack&lt;br /&gt;
&lt;br /&gt;
Therefore, it is suggested that you annotate all your tests with the Frankenstyle name of your plugin.&lt;br /&gt;
&lt;br /&gt;
==== Using multiple phpunit.xml files ====&lt;br /&gt;
&lt;br /&gt;
It&#039;s easy to create alternative phpunit.xml files defining which tests must be run together. For reference, take a look to the default /phpunit.xml available in your base directory once the testing environment has been initialised. After creating the custom file you will be able to run those tests with&lt;br /&gt;
&lt;br /&gt;
 vendor/bin/phpunit -c path/to/your/alternative/phpunit/file.xml&lt;br /&gt;
&lt;br /&gt;
Also, for commodity, you can use this command:&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/phpunit/cli/util.php --buildcomponentconfigs&lt;br /&gt;
&lt;br /&gt;
It will, automatically, create one valid phpunit.xml file within each component (plugin or subsystem) and other important directories, so later you will be able to execute tests like&lt;br /&gt;
&lt;br /&gt;
 vendor/bin/phpunit -c mod/forum[/phpunit.xml]  // Note that it&#039;s not needed to specify the name of the file (if it is &#039;phpunit.xml&#039;).&lt;br /&gt;
 vendor/bin/phpunit -c question&lt;br /&gt;
 vendor/bin/phpunit -c question/type/calculated&lt;br /&gt;
 vendor/bin/phpunit -c backup&lt;br /&gt;
 vendor/bin/phpunit -c lib/dml&lt;br /&gt;
 ...&lt;br /&gt;
&lt;br /&gt;
or, also&lt;br /&gt;
&lt;br /&gt;
 cd directory/with/phpunit.xml&lt;br /&gt;
 phpunit&lt;br /&gt;
&lt;br /&gt;
=External test resources=&lt;br /&gt;
{{Moodle 2.6}}&lt;br /&gt;
By default Moodle phpunit tests contact http://download.moodle.org server when testing curl related functionality. Optionally you may checkout a local copy of the test scripts and access it instead:&lt;br /&gt;
&lt;br /&gt;
# clone https://github.com/moodlehq/moodle-exttests to web directory&lt;br /&gt;
# add to your config.php or modify phpunit.xml file &amp;lt;code php&amp;gt;define(&#039;TEST_EXTERNAL_FILES_HTTP_URL&#039;, &#039;http://localhost/moodle-exttests&#039;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=Writing new tests=&lt;br /&gt;
* read [http://www.phpunit.de/manual/current/en/ official PHPUnit online documentation]&lt;br /&gt;
* see [[Writing PHPUnit tests]]&lt;br /&gt;
&lt;br /&gt;
=Conversion of existing SimpleTests=&lt;br /&gt;
* see [[SimpleTest conversion]]&lt;br /&gt;
&lt;br /&gt;
=PHPUnit support in IDEs=&lt;br /&gt;
&lt;br /&gt;
* [http://www.phpsrc.org/projects/pti-phpunit/wiki/ Eclipse PHP Tool Integration]&lt;br /&gt;
* [http://blog.jetbrains.com/webide/2009/12/phpunit-support/ PHPStorm IDE]&lt;br /&gt;
* [http://netbeans.org/kb/docs/php/phpunit.html Netbeans]&lt;br /&gt;
&lt;br /&gt;
=Common Unit Test Problems=&lt;br /&gt;
[[Common unit test problems]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Unit testing]]&lt;/div&gt;</summary>
		<author><name>Nmagill</name></author>
	</entry>
</feed>