<?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=TimHunt</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=TimHunt"/>
	<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/Special:Contributions/TimHunt"/>
	<updated>2026-06-26T09:57:58Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.43.5</generator>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Process&amp;diff=56915</id>
		<title>Process</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Process&amp;diff=56915"/>
		<updated>2020-02-09T15:58:29Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Submit the code for integration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This document summarises the various development processes used in developing Moodle.  There are four main processes that overlap.&lt;br /&gt;
&lt;br /&gt;
==Integration workflow in the tracker==&lt;br /&gt;
&lt;br /&gt;
The Moodle tracker keeps track of the status of all bug fixes and new features. &lt;br /&gt;
&lt;br /&gt;
We use a workflow that ensures that new code receives multiple reviews by different people before it is included into the core Moodle code.&lt;br /&gt;
&lt;br /&gt;
[[Image:Workflow.jpg]]&lt;br /&gt;
&lt;br /&gt;
A number of roles make this work:&lt;br /&gt;
&lt;br /&gt;
===Users===&lt;br /&gt;
&lt;br /&gt;
Users report bugs and make feature requests directly in the tracker, by creating new issues with a summary and a description.&lt;br /&gt;
&lt;br /&gt;
===Developers===&lt;br /&gt;
&lt;br /&gt;
Developers work on the issues in the tracker to specify solutions and write code that implements these solutions.  They will often ask other developers to &amp;quot;peer review&amp;quot; their code in the early stages to avoid problems later on.&lt;br /&gt;
&lt;br /&gt;
While many of the developers work for Moodle.com, a large number are part of the global development community around Moodle. If you&#039;re interested in becoming a recognised developer, see [[Tracker_guide#Tracker_groups_and_permissions|Tracker groups and permissions]].&lt;br /&gt;
&lt;br /&gt;
===CiBoT===&lt;br /&gt;
&lt;br /&gt;
CiBoT is not a person but a bot who monitors the tracker and performs the [[Automated code review]] when issue is submitted for Peer review or when developer added &#039;&#039;cime&#039;&#039; label.&lt;br /&gt;
&lt;br /&gt;
===Component leads===&lt;br /&gt;
&lt;br /&gt;
[https://tracker.moodle.org/projects/MDL?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page Component leads] are developers with some responsibility for particular components (plugins or modules) in Moodle.  They have authority to decide that a particular fix is suitable and complete enough to be considered for integration in Moodle core and should be called upon to complete peer reviews for code in their components. Note that, apart from that, every component also has some [[Component Leads|HQ Component leads]] that will specifically work on associated issues, triaging, monitoring, reviewing, fixing them.&lt;br /&gt;
&lt;br /&gt;
===Integrators===&lt;br /&gt;
&lt;br /&gt;
On Monday and Tuesday of each week, the integration team (a small team of senior developers employed by Moodle HQ) conducts a code-level review of all issues in the integration queue.  This is often called the &amp;quot;pull&amp;quot; process.  If the fix is judged appropriate they will integrate the code into our git integration repository for further testing and it gets added to the testing queue.&lt;br /&gt;
&lt;br /&gt;
If they find problems they reject the issue and send it back to the developer for further work.&lt;br /&gt;
&lt;br /&gt;
===Testers===&lt;br /&gt;
&lt;br /&gt;
On Wednesday each week, testers look at all the issues in the testing queue, trying each fix and feature to make sure that it does actually fix the problem it was supposed to, and that there are no regressions.&lt;br /&gt;
&lt;br /&gt;
If they find problems they reject the issue and integrators may remove it from the integration repository and push it back to the developer for further work.&lt;br /&gt;
&lt;br /&gt;
See [[Testing of integrated issues]] for more details.&lt;br /&gt;
&lt;br /&gt;
===Production maintainers===&lt;br /&gt;
&lt;br /&gt;
On Thursday each week, production maintainers merge all the issues that passed testing into the git production repository, and it becomes available for use on production systems via git and download packages.&lt;br /&gt;
&lt;br /&gt;
==Stable maintenance cycles==&lt;br /&gt;
&lt;br /&gt;
Moodle releases regular updates of the stable version of the software to fix bugs and other issues.  Releases like 2.2.1, 2.2.2, 2.2.3 etc only include fixes based on the latest major release (2.2) and never any significant new features or database changes.&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ there are teams of developers using the [http://www.scrum.org/ Scrum framework] to work on these issues (as well as new features for [[#Major_release_cycles|major releases]]). &lt;br /&gt;
&lt;br /&gt;
===Minor release (point release) timing===&lt;br /&gt;
&lt;br /&gt;
After [[#Major_release_cycles|major releases]] there will be minor releases.&lt;br /&gt;
* x.x.1 will occur approximately two months after each major release (eg. 2.x).&lt;br /&gt;
* There will then be another point release every two months after that.&lt;br /&gt;
&lt;br /&gt;
See the [[Releases#General_release_calendar|General release calendar]] for details.&lt;br /&gt;
&lt;br /&gt;
===Issue triage===&lt;br /&gt;
&lt;br /&gt;
[[Bug_triage|Issue triage]] involves evaluating new issues, making sure that they are recorded correctly.  One of the most important jobs triagers do is to identify issues that should be fixed in the stable branch. These are set with a priority ranging from &amp;quot;Trivial&amp;quot; up to &amp;quot;Blocker&amp;quot; and other features are checked.&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ there are currently teams working on stable issues (mostly bugs reported by users) and improvements and new features (Partners, Moodle Association, user suggestions and Martin Dougiamas).&lt;br /&gt;
&lt;br /&gt;
===Scrum===&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ, every three weeks, the stable team takes a number of the most urgent issues from the backlog to work on during a period known as a &#039;&#039;sprint&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
At the start of a sprint there is a period of planning and estimation. All issues on the backlog are given a relative rank that is based on issue features including priority, security, Partner interest, patches and votes. Issues are given a relative size in Story Points and these points are summed to allow the teams to determine how many issues they can work on in the sprint.&lt;br /&gt;
&lt;br /&gt;
During the sprint, the team meets daily to discuss solutions and progress, as well as to organise testing and peer reviews of code.  The team has a &#039;&#039;Scrum master&#039;&#039; to help everyone stay organised, to &amp;quot;unblock&amp;quot; any barriers to progress and to protect the team from distracting influences (mostly people attempting to add to the developers&#039; workloads) during the sprint.  The teams&#039; work is documented publicly in the tracker.&lt;br /&gt;
&lt;br /&gt;
Whenever a solution for an issue is finished, it is submitted to the standard integration workflow process described above.&lt;br /&gt;
&lt;br /&gt;
==Major release cycles==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 2.0, we have a policy of release major versions (eg 2.1, 2.2) every six months in May and November. See the [[Releases#General_release_calendar|General release calendar]] for more details.&lt;br /&gt;
&lt;br /&gt;
Each release can be different, but generally the cycles work as follows.&lt;br /&gt;
&lt;br /&gt;
===Define roadmap===&lt;br /&gt;
&lt;br /&gt;
The product owner (Martin Dougiamas) defines the likely roadmap based on community wishes, third-party developments and important issues within the existing code. &lt;br /&gt;
&lt;br /&gt;
Sometimes new features might be based on earlier features, sometimes they may be something developed by a third party that needs to be evaluated and sometimes it might be something completely new.&lt;br /&gt;
&lt;br /&gt;
===Planning and development===&lt;br /&gt;
&lt;br /&gt;
The UX team, employed at Moodle HQ, work on specifications of major new features throughout the cycle, specifying project ahead of development time.&lt;br /&gt;
&lt;br /&gt;
New features are worked on by the &amp;quot;Dev&amp;quot; team. The process of [[#New_feature_development|new feature development]] is described below. When specifications are in place, new code is developed during sprints and goes through the standard weekly integration workflow described above.&lt;br /&gt;
&lt;br /&gt;
===Testing===&lt;br /&gt;
&lt;br /&gt;
During development, as new code is integrated, automated testing conducted at the [[PHPUnit|code]] and [[Acceptance_testing|interface]] levels, to make sure there are no regressions caused by new features.&lt;br /&gt;
&lt;br /&gt;
In the last month before the release, a feature freeze is called (no new features can be added) and volunteer testers from the Moodle community perform manual QA testing of Moodle features. The current set of functional tests is listed in MDLQA-1. The list of tests is extended as new features are added, though we&#039;re also trying to reduce the number as more automated [[Acceptance_testing|acceptance tests]] are developed.&lt;br /&gt;
&lt;br /&gt;
There is also a set of tests for manually testing any major theme changes - MDLQA-11592.&lt;br /&gt;
&lt;br /&gt;
For more details, see [[Testing]].&lt;br /&gt;
&lt;br /&gt;
===Sprints===&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ, development takes place in sprints. The sprints are three-week periods during which developers to focus on a fixed list of issues. Sprints are arranged within each release cycle as shown in the diagram below.&lt;br /&gt;
&lt;br /&gt;
===Events during cycle===&lt;br /&gt;
&lt;br /&gt;
During each cycle there are a periods and events that occur between and around sprints.&lt;br /&gt;
&lt;br /&gt;
[[Image:Dev sprint calendar.png|800px]]&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;Planning and bug fixing&#039;&#039;&#039;&lt;br /&gt;
: A period during which the Roadmap is explored, specs are written and prototypes are created. Regressions in the recent release are fixed as they arise.&lt;br /&gt;
; &#039;&#039;&#039;End sync period&#039;&#039;&#039;&lt;br /&gt;
: During the [[Integration Review#On-sync period|on-sync period]], the recent release and master versions are kept synchronised. No new code is added during this period, which ensures regressions are fixed rapidly. This also allows for planning and provides relief for developers after a release.&lt;br /&gt;
; &#039;&#039;&#039;Personal projects&#039;&#039;&#039;&lt;br /&gt;
: Affecting full-time HQ developers only, this period allows for individual creations to be explored and provides a break from sprints.&lt;br /&gt;
; &#039;&#039;&#039;Code freeze&#039;&#039;&#039;&lt;br /&gt;
: A point after which no new code (only fixes to existing code) is accepted until beyond the release. This stabilisation allows for QA testing.&lt;br /&gt;
; &#039;&#039;&#039;QA, bug fixing, continuous integration&#039;&#039;&#039;&lt;br /&gt;
: A period after the code freeze where quality assurance testing takes place. No new code is added, which means developers are able to respond rapidly to bugs found. Integration becomes [[Integration Review#During continuous integration/Freeze/QA period|continuous]], meaning that failed QA tests can be re-run within days rather than having to wait for the weekly release.&lt;br /&gt;
; &#039;&#039;&#039;Release candidate&#039;&#039;&#039;&lt;br /&gt;
: A point prior to the full release where a candidate is made public for wider testing.&lt;br /&gt;
&lt;br /&gt;
==New feature development==&lt;br /&gt;
&lt;br /&gt;
Major new features in Moodle usually should go through the following process.&lt;br /&gt;
&lt;br /&gt;
===Specification===&lt;br /&gt;
&lt;br /&gt;
The User Experience (UX) team should create detailed wireframes and features and goals for the new feature. It should be agreed upon and as final as possible before development starts.&lt;br /&gt;
&lt;br /&gt;
Developers should create a detailed spec (here in the developer docs) outlining their goals for the development and their design for meeting those goals.  The more detail the better.&lt;br /&gt;
&lt;br /&gt;
Developers should also create an issue in the tracker (linking to your docs) to keep track of the project status.&lt;br /&gt;
&lt;br /&gt;
===Community consultation===&lt;br /&gt;
&lt;br /&gt;
Get the community involved in looking at the spec to see if it meets their needs and to get further feedback.  Please post in the [http://moodle.org/mod/forum/view.php?id=8052 Future major features forum] on moodle.org. You could also blog/tweet about it etc.&lt;br /&gt;
&lt;br /&gt;
Community developers proposing a new feature will want to talk with HQ core developers to make sure the ideas make sense, and possibly get some review on database design, architecture and so on.&lt;br /&gt;
&lt;br /&gt;
===Develop the code using Git===&lt;br /&gt;
&lt;br /&gt;
Develop your code on an open Git repository, like github.com.  That enables people to see your code and to help you as it develops.  Testers and early adopters also have the opportunity to try it early in the process and give you more valuable feedback.&lt;br /&gt;
&lt;br /&gt;
Coverage with automated tests ([[PHPUnit]] or [[Behat|Behat_integration]]) is mandatory for new features.&lt;br /&gt;
&lt;br /&gt;
It is essential that your code follows the [[Coding|Moodle Coding Guide]].&lt;br /&gt;
&lt;br /&gt;
===Submit your code for peer review===&lt;br /&gt;
&lt;br /&gt;
Click on &amp;quot;Request peer review&amp;quot; button in the tracker.&lt;br /&gt;
&lt;br /&gt;
You need to fill in the information about your public git repository and which branches the fixes are on. Make sure you are listed as Assignee.&lt;br /&gt;
&lt;br /&gt;
This would be a good time to fill in the testing instructions (read the [[Testing instructions guide|instructions guide]]) for how to verify your fix is correct. You may also wish to add a comment in the bug.&lt;br /&gt;
&lt;br /&gt;
Component leads should put issues, which affect code in their components, up for peer review to allow interested parties to provide feedback. However, if it is not reviewed in a week, a component lead may send the issue to integration. If integrators consider that the issue has not been given proper chance for peer review (e.g. is extremely large or complex...) it can be decided to move the issue back in the process.&lt;br /&gt;
&lt;br /&gt;
All other developers, including people who are component leads but working outside their component, should have their issues peer reviewed before they are sent to integration.&lt;br /&gt;
&lt;br /&gt;
===Peer review===&lt;br /&gt;
&lt;br /&gt;
The [http://tracker.moodle.org/browse/MDL#selectedTab=com.atlassian.jira.plugin.system.project%3Acomponents-panel component lead] should peer-review the change. If there is no component lead for an affected component, any other recognised developer may complete the peer review. The peer reviewer will either give you comments on the code and if it needs more work.&lt;br /&gt;
&lt;br /&gt;
Process and the list of things to check are described in [[Peer reviewing]].&lt;br /&gt;
&lt;br /&gt;
===Submit the code for integration===&lt;br /&gt;
&lt;br /&gt;
The developer is responsible for acting on the feedback from the peer reviewer. If changes have been made and the developer is satisfied that this has accommodated the feedback from the peer reviewer, then the developer can submit the issue for integration. If there have been significant changes after the peer review, or if the peer reviewer has raised concerns about the approach taken, then the developer should offer the issue up for peer review again, most often to the same peer reviewer, but not necessarily.&lt;br /&gt;
&lt;br /&gt;
Submitting an issue to integration is much the same as for any Moodle code.  See [[Integration Review]] and the information about the integration workflow above.&lt;br /&gt;
&lt;br /&gt;
==Fixing a bug==&lt;br /&gt;
&lt;br /&gt;
Bug fixes, and minor features or enhancements should go through the following process. (The only exception is English language string typo fixes or suggested improvements, which may be contributed to the en_fix language pack on the [http://lang.moodle.org/ Moodle translation site].)&lt;br /&gt;
&lt;br /&gt;
===Make sure there is a tracker issue===&lt;br /&gt;
&lt;br /&gt;
Every change must have an issue in the tracker. If you are fixing a bug, there is probably one there already, but if not, create one. [[Tracker tips|Tips for searching tracker]].&lt;br /&gt;
&lt;br /&gt;
===Decide which branches the fix is required on===&lt;br /&gt;
&lt;br /&gt;
Bugs should normally be fixed on all the supported stable branches that are affected. New features should just go into master, but sometimes minor enhancements are made on the most recent stable branch.&lt;br /&gt;
&lt;br /&gt;
===Develop your change using git===&lt;br /&gt;
&lt;br /&gt;
Develop your fix and push the change to an open git repository, for example on github.com. See also [[Git for developers]]&lt;br /&gt;
&lt;br /&gt;
It is essential that your code follows the [[Coding|Moodle Coding Guide]].&lt;br /&gt;
&lt;br /&gt;
You will need to push one commit for each branch the fix needs to be applied to. Often people use branch names like MDL-12345-31_brief_name so it is clear what each branch is. [http://kernel.org/pub/software/scm/git/docs/git-cherry-pick.html git cherry-pick] can help with replicating the fix onto different branches.&lt;br /&gt;
&lt;br /&gt;
===Submit your code for peer review===&lt;br /&gt;
&lt;br /&gt;
Once your fix is done, it should be submitted for a peer review.&lt;br /&gt;
&lt;br /&gt;
The following information is necessary for this:&lt;br /&gt;
* Information about your public git repository&lt;br /&gt;
** repository URL&lt;br /&gt;
** branch name(s)&lt;br /&gt;
** diff URL&lt;br /&gt;
* [[Testing instructions guide|Testing instructions]] for how to verify your fix is correct.&lt;br /&gt;
&lt;br /&gt;
If you have never contributed to Moodle and don&#039;t see a button &amp;quot;Request peer review&amp;quot;, just comment on the issue with the above information. The component lead or another user with sufficient privileges will then send the issue up for peer review for you.&lt;br /&gt;
&lt;br /&gt;
After your first fix is integrated you will be added to developers group and will be able to send issues for peer review yourself. In this case make sure you are listed as Assignee and click on &amp;quot;Request peer review&amp;quot; button in the tracker&lt;br /&gt;
&lt;br /&gt;
===Peer review===&lt;br /&gt;
&lt;br /&gt;
The [https://tracker.moodle.org/projects/MDL?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page component lead] should peer-review the change. If there is no component lead for an affected component, any other recognised developer may complete the peer review. The peer reviewer will either give you comments on the code and if it needs more work.&lt;br /&gt;
&lt;br /&gt;
Process and the list of things to check are described in [[Peer reviewing]].&lt;br /&gt;
&lt;br /&gt;
===Submit your code for integration===&lt;br /&gt;
&lt;br /&gt;
It will then be reviewed the following week by one of the integration team and either integrated or rejected. Once integrated, the fix will be tested, and then included in the next weekly release. For details see [[Integration Review]].&lt;br /&gt;
&lt;br /&gt;
==Security issues==&lt;br /&gt;
&lt;br /&gt;
Issues identified as [[Security|security issues]] are resolved in a slightly different way, in order to achieve responsible disclosure as described in [[Moodle security procedures]].&lt;br /&gt;
&lt;br /&gt;
* Security issues should be labelled as &amp;quot;Minor&amp;quot; or &amp;quot;Serious&amp;quot; in order control visibility of the issue.&lt;br /&gt;
** An issue reported with a security level of &amp;quot;Could be a security issue&amp;quot; should be evaluated as soon as possible and either set as &amp;quot;Minor&amp;quot; or &amp;quot;Serious&amp;quot; or the security level should be set to &amp;quot;None&amp;quot;.&lt;br /&gt;
* Solutions to security issues should not:&lt;br /&gt;
** be made available in public repositories.&lt;br /&gt;
*** If a developer has shared a solution as Git branches via Github, they should be asked to provide the solutions as [[How_to_create_a_patch|stand-alone patches]] attached to the issue and to [[#How to remove a branch from Github|remove the solution from Github]].&lt;br /&gt;
** contain details about the security problem in the commit message.&lt;br /&gt;
*** Instead use generic terms like, &amp;quot;improve&amp;quot;, &amp;quot;better handling of&amp;quot; ..&lt;br /&gt;
* The solution will not be integrated until the week before a [[Process#Stable_maintenance_cycles|minor release]] following the normal [[Release process|Release process]]. In short, the issue will be incorporated into the integration version, rebased, tested and made ready for release as a normal issue would, but not until as late as possible.&lt;br /&gt;
* Details of security issues will be shared with registered admins with the minor release.&lt;br /&gt;
* Details of security issues will not be publicly announced until one week after a minor release to allow admins to update.&lt;br /&gt;
&lt;br /&gt;
Note that not all the labelled (minor) security issues are always handled following the procedure above. It&#039;s possible that, after discussion, it&#039;s decided a given issue is not a real Moodle security problem (say external disclosures/potential attacks using Moodle as vector, not as target, discussions revealing some private details...). Those issues will be processed as normal issues, generating the needed user documentation if necessary and will be part of the habitual weekly releases.&lt;br /&gt;
&lt;br /&gt;
====How to remove a branch from Github====&lt;br /&gt;
&lt;br /&gt;
To remove a branch from Github, you can use the following command.&lt;br /&gt;
&lt;br /&gt;
 git push github :remote_branch&lt;br /&gt;
&lt;br /&gt;
Where &#039;&#039;remote_branch&#039;&#039; is the name of your remote branch, for example &#039;wip-mdl-1234&#039;. This effectively replaces the remote branch with nothing, removing the remote branch, but leaving the branch intact in your local Git repository. Please note that its likely that your commit will still exist on github due to the nature of git, so its best to avoid doing this in the first place.&lt;br /&gt;
&lt;br /&gt;
==Policy issues==&lt;br /&gt;
&lt;br /&gt;
Occasionally within Moodle we run into policy issues where a high-level decision needs to be made about how things are to be done.&lt;br /&gt;
&lt;br /&gt;
In these cases the process is as follows:&lt;br /&gt;
* Create an issue in the tracker with a [https://tracker.moodle.org/browse/MDL/component/12733 Policy component] and put &amp;quot;POLICY:&amp;quot; as a prefix on the summary.&lt;br /&gt;
* In the description describe the problem clearly as well as all the options.  If it&#039;s long then make a page here in Moodle Dev Docs and link to it.&lt;br /&gt;
* Do not use this issue for code.  If there are issues that depend on this policy decision, then add tracker links to them as dependencies.&lt;br /&gt;
* Feel free to encourage people to come and talk about the policy to support various points of view.  The more evidence we have (from everyone in the community) the better.&lt;br /&gt;
&lt;br /&gt;
Some time has been scheduled in the weekly Moodle HQ meeting to look at Policy issues and try to make decisions on them.  We discuss all the evidence and try to achieve a high amount of consensus.  Deadlocked issues can be resolved by a decision from Martin Dougiamas (this is rarely needed).&lt;br /&gt;
&lt;br /&gt;
Decisions will be posted on the issue and that issue will be closed, allowing any dependent issues to continue to integration (or not).   Decisions are final and bribes hardly ever work.&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Release process]]&lt;br /&gt;
* [[Deprecation]]&lt;br /&gt;
* [http://tracker.moodle.org/secure/Dashboard.jspa?selectPageId=11350 Integration dashboard]&lt;br /&gt;
&lt;br /&gt;
Walks-though of the process for contributors:&lt;br /&gt;
* By Dan Poltawski, Integrator: http://www.slideshare.net/poltawski/how-to-guarantee-your-change-is-integrated-to-moodle-core, https://www.youtube.com/watch?v=836WtnM2YpM&lt;br /&gt;
* By Tim Hunt, contributor: http://tjhunt.blogspot.co.uk/2012/03/fixing-bug-in-moodle-core-mechanics.html and https://www.youtube.com/watch?v=gPPA3h7OGQU&lt;br /&gt;
&lt;br /&gt;
[[Category:Processes]]&lt;br /&gt;
[[Category:Quality Assurance]]&lt;br /&gt;
[[Category:Git]]&lt;br /&gt;
[[Category:Core development]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Process&amp;diff=56914</id>
		<title>Process</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Process&amp;diff=56914"/>
		<updated>2020-02-09T15:58:00Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Submit your code for integration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This document summarises the various development processes used in developing Moodle.  There are four main processes that overlap.&lt;br /&gt;
&lt;br /&gt;
==Integration workflow in the tracker==&lt;br /&gt;
&lt;br /&gt;
The Moodle tracker keeps track of the status of all bug fixes and new features. &lt;br /&gt;
&lt;br /&gt;
We use a workflow that ensures that new code receives multiple reviews by different people before it is included into the core Moodle code.&lt;br /&gt;
&lt;br /&gt;
[[Image:Workflow.jpg]]&lt;br /&gt;
&lt;br /&gt;
A number of roles make this work:&lt;br /&gt;
&lt;br /&gt;
===Users===&lt;br /&gt;
&lt;br /&gt;
Users report bugs and make feature requests directly in the tracker, by creating new issues with a summary and a description.&lt;br /&gt;
&lt;br /&gt;
===Developers===&lt;br /&gt;
&lt;br /&gt;
Developers work on the issues in the tracker to specify solutions and write code that implements these solutions.  They will often ask other developers to &amp;quot;peer review&amp;quot; their code in the early stages to avoid problems later on.&lt;br /&gt;
&lt;br /&gt;
While many of the developers work for Moodle.com, a large number are part of the global development community around Moodle. If you&#039;re interested in becoming a recognised developer, see [[Tracker_guide#Tracker_groups_and_permissions|Tracker groups and permissions]].&lt;br /&gt;
&lt;br /&gt;
===CiBoT===&lt;br /&gt;
&lt;br /&gt;
CiBoT is not a person but a bot who monitors the tracker and performs the [[Automated code review]] when issue is submitted for Peer review or when developer added &#039;&#039;cime&#039;&#039; label.&lt;br /&gt;
&lt;br /&gt;
===Component leads===&lt;br /&gt;
&lt;br /&gt;
[https://tracker.moodle.org/projects/MDL?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page Component leads] are developers with some responsibility for particular components (plugins or modules) in Moodle.  They have authority to decide that a particular fix is suitable and complete enough to be considered for integration in Moodle core and should be called upon to complete peer reviews for code in their components. Note that, apart from that, every component also has some [[Component Leads|HQ Component leads]] that will specifically work on associated issues, triaging, monitoring, reviewing, fixing them.&lt;br /&gt;
&lt;br /&gt;
===Integrators===&lt;br /&gt;
&lt;br /&gt;
On Monday and Tuesday of each week, the integration team (a small team of senior developers employed by Moodle HQ) conducts a code-level review of all issues in the integration queue.  This is often called the &amp;quot;pull&amp;quot; process.  If the fix is judged appropriate they will integrate the code into our git integration repository for further testing and it gets added to the testing queue.&lt;br /&gt;
&lt;br /&gt;
If they find problems they reject the issue and send it back to the developer for further work.&lt;br /&gt;
&lt;br /&gt;
===Testers===&lt;br /&gt;
&lt;br /&gt;
On Wednesday each week, testers look at all the issues in the testing queue, trying each fix and feature to make sure that it does actually fix the problem it was supposed to, and that there are no regressions.&lt;br /&gt;
&lt;br /&gt;
If they find problems they reject the issue and integrators may remove it from the integration repository and push it back to the developer for further work.&lt;br /&gt;
&lt;br /&gt;
See [[Testing of integrated issues]] for more details.&lt;br /&gt;
&lt;br /&gt;
===Production maintainers===&lt;br /&gt;
&lt;br /&gt;
On Thursday each week, production maintainers merge all the issues that passed testing into the git production repository, and it becomes available for use on production systems via git and download packages.&lt;br /&gt;
&lt;br /&gt;
==Stable maintenance cycles==&lt;br /&gt;
&lt;br /&gt;
Moodle releases regular updates of the stable version of the software to fix bugs and other issues.  Releases like 2.2.1, 2.2.2, 2.2.3 etc only include fixes based on the latest major release (2.2) and never any significant new features or database changes.&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ there are teams of developers using the [http://www.scrum.org/ Scrum framework] to work on these issues (as well as new features for [[#Major_release_cycles|major releases]]). &lt;br /&gt;
&lt;br /&gt;
===Minor release (point release) timing===&lt;br /&gt;
&lt;br /&gt;
After [[#Major_release_cycles|major releases]] there will be minor releases.&lt;br /&gt;
* x.x.1 will occur approximately two months after each major release (eg. 2.x).&lt;br /&gt;
* There will then be another point release every two months after that.&lt;br /&gt;
&lt;br /&gt;
See the [[Releases#General_release_calendar|General release calendar]] for details.&lt;br /&gt;
&lt;br /&gt;
===Issue triage===&lt;br /&gt;
&lt;br /&gt;
[[Bug_triage|Issue triage]] involves evaluating new issues, making sure that they are recorded correctly.  One of the most important jobs triagers do is to identify issues that should be fixed in the stable branch. These are set with a priority ranging from &amp;quot;Trivial&amp;quot; up to &amp;quot;Blocker&amp;quot; and other features are checked.&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ there are currently teams working on stable issues (mostly bugs reported by users) and improvements and new features (Partners, Moodle Association, user suggestions and Martin Dougiamas).&lt;br /&gt;
&lt;br /&gt;
===Scrum===&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ, every three weeks, the stable team takes a number of the most urgent issues from the backlog to work on during a period known as a &#039;&#039;sprint&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
At the start of a sprint there is a period of planning and estimation. All issues on the backlog are given a relative rank that is based on issue features including priority, security, Partner interest, patches and votes. Issues are given a relative size in Story Points and these points are summed to allow the teams to determine how many issues they can work on in the sprint.&lt;br /&gt;
&lt;br /&gt;
During the sprint, the team meets daily to discuss solutions and progress, as well as to organise testing and peer reviews of code.  The team has a &#039;&#039;Scrum master&#039;&#039; to help everyone stay organised, to &amp;quot;unblock&amp;quot; any barriers to progress and to protect the team from distracting influences (mostly people attempting to add to the developers&#039; workloads) during the sprint.  The teams&#039; work is documented publicly in the tracker.&lt;br /&gt;
&lt;br /&gt;
Whenever a solution for an issue is finished, it is submitted to the standard integration workflow process described above.&lt;br /&gt;
&lt;br /&gt;
==Major release cycles==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 2.0, we have a policy of release major versions (eg 2.1, 2.2) every six months in May and November. See the [[Releases#General_release_calendar|General release calendar]] for more details.&lt;br /&gt;
&lt;br /&gt;
Each release can be different, but generally the cycles work as follows.&lt;br /&gt;
&lt;br /&gt;
===Define roadmap===&lt;br /&gt;
&lt;br /&gt;
The product owner (Martin Dougiamas) defines the likely roadmap based on community wishes, third-party developments and important issues within the existing code. &lt;br /&gt;
&lt;br /&gt;
Sometimes new features might be based on earlier features, sometimes they may be something developed by a third party that needs to be evaluated and sometimes it might be something completely new.&lt;br /&gt;
&lt;br /&gt;
===Planning and development===&lt;br /&gt;
&lt;br /&gt;
The UX team, employed at Moodle HQ, work on specifications of major new features throughout the cycle, specifying project ahead of development time.&lt;br /&gt;
&lt;br /&gt;
New features are worked on by the &amp;quot;Dev&amp;quot; team. The process of [[#New_feature_development|new feature development]] is described below. When specifications are in place, new code is developed during sprints and goes through the standard weekly integration workflow described above.&lt;br /&gt;
&lt;br /&gt;
===Testing===&lt;br /&gt;
&lt;br /&gt;
During development, as new code is integrated, automated testing conducted at the [[PHPUnit|code]] and [[Acceptance_testing|interface]] levels, to make sure there are no regressions caused by new features.&lt;br /&gt;
&lt;br /&gt;
In the last month before the release, a feature freeze is called (no new features can be added) and volunteer testers from the Moodle community perform manual QA testing of Moodle features. The current set of functional tests is listed in MDLQA-1. The list of tests is extended as new features are added, though we&#039;re also trying to reduce the number as more automated [[Acceptance_testing|acceptance tests]] are developed.&lt;br /&gt;
&lt;br /&gt;
There is also a set of tests for manually testing any major theme changes - MDLQA-11592.&lt;br /&gt;
&lt;br /&gt;
For more details, see [[Testing]].&lt;br /&gt;
&lt;br /&gt;
===Sprints===&lt;br /&gt;
&lt;br /&gt;
At Moodle HQ, development takes place in sprints. The sprints are three-week periods during which developers to focus on a fixed list of issues. Sprints are arranged within each release cycle as shown in the diagram below.&lt;br /&gt;
&lt;br /&gt;
===Events during cycle===&lt;br /&gt;
&lt;br /&gt;
During each cycle there are a periods and events that occur between and around sprints.&lt;br /&gt;
&lt;br /&gt;
[[Image:Dev sprint calendar.png|800px]]&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;Planning and bug fixing&#039;&#039;&#039;&lt;br /&gt;
: A period during which the Roadmap is explored, specs are written and prototypes are created. Regressions in the recent release are fixed as they arise.&lt;br /&gt;
; &#039;&#039;&#039;End sync period&#039;&#039;&#039;&lt;br /&gt;
: During the [[Integration Review#On-sync period|on-sync period]], the recent release and master versions are kept synchronised. No new code is added during this period, which ensures regressions are fixed rapidly. This also allows for planning and provides relief for developers after a release.&lt;br /&gt;
; &#039;&#039;&#039;Personal projects&#039;&#039;&#039;&lt;br /&gt;
: Affecting full-time HQ developers only, this period allows for individual creations to be explored and provides a break from sprints.&lt;br /&gt;
; &#039;&#039;&#039;Code freeze&#039;&#039;&#039;&lt;br /&gt;
: A point after which no new code (only fixes to existing code) is accepted until beyond the release. This stabilisation allows for QA testing.&lt;br /&gt;
; &#039;&#039;&#039;QA, bug fixing, continuous integration&#039;&#039;&#039;&lt;br /&gt;
: A period after the code freeze where quality assurance testing takes place. No new code is added, which means developers are able to respond rapidly to bugs found. Integration becomes [[Integration Review#During continuous integration/Freeze/QA period|continuous]], meaning that failed QA tests can be re-run within days rather than having to wait for the weekly release.&lt;br /&gt;
; &#039;&#039;&#039;Release candidate&#039;&#039;&#039;&lt;br /&gt;
: A point prior to the full release where a candidate is made public for wider testing.&lt;br /&gt;
&lt;br /&gt;
==New feature development==&lt;br /&gt;
&lt;br /&gt;
Major new features in Moodle usually should go through the following process.&lt;br /&gt;
&lt;br /&gt;
===Specification===&lt;br /&gt;
&lt;br /&gt;
The User Experience (UX) team should create detailed wireframes and features and goals for the new feature. It should be agreed upon and as final as possible before development starts.&lt;br /&gt;
&lt;br /&gt;
Developers should create a detailed spec (here in the developer docs) outlining their goals for the development and their design for meeting those goals.  The more detail the better.&lt;br /&gt;
&lt;br /&gt;
Developers should also create an issue in the tracker (linking to your docs) to keep track of the project status.&lt;br /&gt;
&lt;br /&gt;
===Community consultation===&lt;br /&gt;
&lt;br /&gt;
Get the community involved in looking at the spec to see if it meets their needs and to get further feedback.  Please post in the [http://moodle.org/mod/forum/view.php?id=8052 Future major features forum] on moodle.org. You could also blog/tweet about it etc.&lt;br /&gt;
&lt;br /&gt;
Community developers proposing a new feature will want to talk with HQ core developers to make sure the ideas make sense, and possibly get some review on database design, architecture and so on.&lt;br /&gt;
&lt;br /&gt;
===Develop the code using Git===&lt;br /&gt;
&lt;br /&gt;
Develop your code on an open Git repository, like github.com.  That enables people to see your code and to help you as it develops.  Testers and early adopters also have the opportunity to try it early in the process and give you more valuable feedback.&lt;br /&gt;
&lt;br /&gt;
Coverage with automated tests ([[PHPUnit]] or [[Behat|Behat_integration]]) is mandatory for new features.&lt;br /&gt;
&lt;br /&gt;
It is essential that your code follows the [[Coding|Moodle Coding Guide]].&lt;br /&gt;
&lt;br /&gt;
===Submit your code for peer review===&lt;br /&gt;
&lt;br /&gt;
Click on &amp;quot;Request peer review&amp;quot; button in the tracker.&lt;br /&gt;
&lt;br /&gt;
You need to fill in the information about your public git repository and which branches the fixes are on. Make sure you are listed as Assignee.&lt;br /&gt;
&lt;br /&gt;
This would be a good time to fill in the testing instructions (read the [[Testing instructions guide|instructions guide]]) for how to verify your fix is correct. You may also wish to add a comment in the bug.&lt;br /&gt;
&lt;br /&gt;
Component leads should put issues, which affect code in their components, up for peer review to allow interested parties to provide feedback. However, if it is not reviewed in a week, a component lead may send the issue to integration. If integrators consider that the issue has not been given proper chance for peer review (e.g. is extremely large or complex...) it can be decided to move the issue back in the process.&lt;br /&gt;
&lt;br /&gt;
All other developers, including people who are component leads but working outside their component, should have their issues peer reviewed before they are sent to integration.&lt;br /&gt;
&lt;br /&gt;
===Peer review===&lt;br /&gt;
&lt;br /&gt;
The [http://tracker.moodle.org/browse/MDL#selectedTab=com.atlassian.jira.plugin.system.project%3Acomponents-panel component lead] should peer-review the change. If there is no component lead for an affected component, any other recognised developer may complete the peer review. The peer reviewer will either give you comments on the code and if it needs more work.&lt;br /&gt;
&lt;br /&gt;
Process and the list of things to check are described in [[Peer reviewing]].&lt;br /&gt;
&lt;br /&gt;
===Submit the code for integration===&lt;br /&gt;
&lt;br /&gt;
The developer is responsible for acting on the feedback from the peer reviewer. If changes have been made and the developer is satisfied that this has accommodated the feedback from the peer reviewer, then the developer can submit the issue for integration. If there have been significant changes after the peer review, or if the peer reviewer has raised concerns about the approach taken, then the developer should offer the issue up for peer review again, most often to the same peer reviewer, but not necessarily.&lt;br /&gt;
&lt;br /&gt;
Submitting an issue to integration is much the same as for any Moodle code.  See the information about the integration workflow above.&lt;br /&gt;
&lt;br /&gt;
==Fixing a bug==&lt;br /&gt;
&lt;br /&gt;
Bug fixes, and minor features or enhancements should go through the following process. (The only exception is English language string typo fixes or suggested improvements, which may be contributed to the en_fix language pack on the [http://lang.moodle.org/ Moodle translation site].)&lt;br /&gt;
&lt;br /&gt;
===Make sure there is a tracker issue===&lt;br /&gt;
&lt;br /&gt;
Every change must have an issue in the tracker. If you are fixing a bug, there is probably one there already, but if not, create one. [[Tracker tips|Tips for searching tracker]].&lt;br /&gt;
&lt;br /&gt;
===Decide which branches the fix is required on===&lt;br /&gt;
&lt;br /&gt;
Bugs should normally be fixed on all the supported stable branches that are affected. New features should just go into master, but sometimes minor enhancements are made on the most recent stable branch.&lt;br /&gt;
&lt;br /&gt;
===Develop your change using git===&lt;br /&gt;
&lt;br /&gt;
Develop your fix and push the change to an open git repository, for example on github.com. See also [[Git for developers]]&lt;br /&gt;
&lt;br /&gt;
It is essential that your code follows the [[Coding|Moodle Coding Guide]].&lt;br /&gt;
&lt;br /&gt;
You will need to push one commit for each branch the fix needs to be applied to. Often people use branch names like MDL-12345-31_brief_name so it is clear what each branch is. [http://kernel.org/pub/software/scm/git/docs/git-cherry-pick.html git cherry-pick] can help with replicating the fix onto different branches.&lt;br /&gt;
&lt;br /&gt;
===Submit your code for peer review===&lt;br /&gt;
&lt;br /&gt;
Once your fix is done, it should be submitted for a peer review.&lt;br /&gt;
&lt;br /&gt;
The following information is necessary for this:&lt;br /&gt;
* Information about your public git repository&lt;br /&gt;
** repository URL&lt;br /&gt;
** branch name(s)&lt;br /&gt;
** diff URL&lt;br /&gt;
* [[Testing instructions guide|Testing instructions]] for how to verify your fix is correct.&lt;br /&gt;
&lt;br /&gt;
If you have never contributed to Moodle and don&#039;t see a button &amp;quot;Request peer review&amp;quot;, just comment on the issue with the above information. The component lead or another user with sufficient privileges will then send the issue up for peer review for you.&lt;br /&gt;
&lt;br /&gt;
After your first fix is integrated you will be added to developers group and will be able to send issues for peer review yourself. In this case make sure you are listed as Assignee and click on &amp;quot;Request peer review&amp;quot; button in the tracker&lt;br /&gt;
&lt;br /&gt;
===Peer review===&lt;br /&gt;
&lt;br /&gt;
The [https://tracker.moodle.org/projects/MDL?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page component lead] should peer-review the change. If there is no component lead for an affected component, any other recognised developer may complete the peer review. The peer reviewer will either give you comments on the code and if it needs more work.&lt;br /&gt;
&lt;br /&gt;
Process and the list of things to check are described in [[Peer reviewing]].&lt;br /&gt;
&lt;br /&gt;
===Submit your code for integration===&lt;br /&gt;
&lt;br /&gt;
It will then be reviewed the following week by one of the integration team and either integrated or rejected. Once integrated, the fix will be tested, and then included in the next weekly release. For details see [[Integration Review]].&lt;br /&gt;
&lt;br /&gt;
==Security issues==&lt;br /&gt;
&lt;br /&gt;
Issues identified as [[Security|security issues]] are resolved in a slightly different way, in order to achieve responsible disclosure as described in [[Moodle security procedures]].&lt;br /&gt;
&lt;br /&gt;
* Security issues should be labelled as &amp;quot;Minor&amp;quot; or &amp;quot;Serious&amp;quot; in order control visibility of the issue.&lt;br /&gt;
** An issue reported with a security level of &amp;quot;Could be a security issue&amp;quot; should be evaluated as soon as possible and either set as &amp;quot;Minor&amp;quot; or &amp;quot;Serious&amp;quot; or the security level should be set to &amp;quot;None&amp;quot;.&lt;br /&gt;
* Solutions to security issues should not:&lt;br /&gt;
** be made available in public repositories.&lt;br /&gt;
*** If a developer has shared a solution as Git branches via Github, they should be asked to provide the solutions as [[How_to_create_a_patch|stand-alone patches]] attached to the issue and to [[#How to remove a branch from Github|remove the solution from Github]].&lt;br /&gt;
** contain details about the security problem in the commit message.&lt;br /&gt;
*** Instead use generic terms like, &amp;quot;improve&amp;quot;, &amp;quot;better handling of&amp;quot; ..&lt;br /&gt;
* The solution will not be integrated until the week before a [[Process#Stable_maintenance_cycles|minor release]] following the normal [[Release process|Release process]]. In short, the issue will be incorporated into the integration version, rebased, tested and made ready for release as a normal issue would, but not until as late as possible.&lt;br /&gt;
* Details of security issues will be shared with registered admins with the minor release.&lt;br /&gt;
* Details of security issues will not be publicly announced until one week after a minor release to allow admins to update.&lt;br /&gt;
&lt;br /&gt;
Note that not all the labelled (minor) security issues are always handled following the procedure above. It&#039;s possible that, after discussion, it&#039;s decided a given issue is not a real Moodle security problem (say external disclosures/potential attacks using Moodle as vector, not as target, discussions revealing some private details...). Those issues will be processed as normal issues, generating the needed user documentation if necessary and will be part of the habitual weekly releases.&lt;br /&gt;
&lt;br /&gt;
====How to remove a branch from Github====&lt;br /&gt;
&lt;br /&gt;
To remove a branch from Github, you can use the following command.&lt;br /&gt;
&lt;br /&gt;
 git push github :remote_branch&lt;br /&gt;
&lt;br /&gt;
Where &#039;&#039;remote_branch&#039;&#039; is the name of your remote branch, for example &#039;wip-mdl-1234&#039;. This effectively replaces the remote branch with nothing, removing the remote branch, but leaving the branch intact in your local Git repository. Please note that its likely that your commit will still exist on github due to the nature of git, so its best to avoid doing this in the first place.&lt;br /&gt;
&lt;br /&gt;
==Policy issues==&lt;br /&gt;
&lt;br /&gt;
Occasionally within Moodle we run into policy issues where a high-level decision needs to be made about how things are to be done.&lt;br /&gt;
&lt;br /&gt;
In these cases the process is as follows:&lt;br /&gt;
* Create an issue in the tracker with a [https://tracker.moodle.org/browse/MDL/component/12733 Policy component] and put &amp;quot;POLICY:&amp;quot; as a prefix on the summary.&lt;br /&gt;
* In the description describe the problem clearly as well as all the options.  If it&#039;s long then make a page here in Moodle Dev Docs and link to it.&lt;br /&gt;
* Do not use this issue for code.  If there are issues that depend on this policy decision, then add tracker links to them as dependencies.&lt;br /&gt;
* Feel free to encourage people to come and talk about the policy to support various points of view.  The more evidence we have (from everyone in the community) the better.&lt;br /&gt;
&lt;br /&gt;
Some time has been scheduled in the weekly Moodle HQ meeting to look at Policy issues and try to make decisions on them.  We discuss all the evidence and try to achieve a high amount of consensus.  Deadlocked issues can be resolved by a decision from Martin Dougiamas (this is rarely needed).&lt;br /&gt;
&lt;br /&gt;
Decisions will be posted on the issue and that issue will be closed, allowing any dependent issues to continue to integration (or not).   Decisions are final and bribes hardly ever work.&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Release process]]&lt;br /&gt;
* [[Deprecation]]&lt;br /&gt;
* [http://tracker.moodle.org/secure/Dashboard.jspa?selectPageId=11350 Integration dashboard]&lt;br /&gt;
&lt;br /&gt;
Walks-though of the process for contributors:&lt;br /&gt;
* By Dan Poltawski, Integrator: http://www.slideshare.net/poltawski/how-to-guarantee-your-change-is-integrated-to-moodle-core, https://www.youtube.com/watch?v=836WtnM2YpM&lt;br /&gt;
* By Tim Hunt, contributor: http://tjhunt.blogspot.co.uk/2012/03/fixing-bug-in-moodle-core-mechanics.html and https://www.youtube.com/watch?v=gPPA3h7OGQU&lt;br /&gt;
&lt;br /&gt;
[[Category:Processes]]&lt;br /&gt;
[[Category:Quality Assurance]]&lt;br /&gt;
[[Category:Git]]&lt;br /&gt;
[[Category:Core development]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Activity_chooser&amp;diff=56654</id>
		<title>Activity chooser</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Activity_chooser&amp;diff=56654"/>
		<updated>2019-11-15T07:48:36Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Adding activity items to the chooser through plugins */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Infobox Project&lt;br /&gt;
|state = Open&lt;br /&gt;
|name = Activity chooser&lt;br /&gt;
|tracker = MDL-67255 (epic), MDL-57828, MDL-61511&lt;br /&gt;
|discussion = https://moodle.org/mod/forum/discuss.php?d=346664&lt;br /&gt;
|assignee = Team Alpha&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
&lt;br /&gt;
Through our road-map creation process of looking at highly voted tracker issues and relevant forum posts, as well as MUA interaction, an update to the activity chooser to simplify and make less intimidating, was chosen.&lt;br /&gt;
&lt;br /&gt;
MDL-57828 was created and worked on, but unfortunately stalled, and did not complete its way through the integration process. There is a substantial forum post [https://moodle.org/mod/forum/discuss.php?d=346664], with many suggestions and current pain points with the activity chooser. The MUA created a proposal issue (MDL-61511) to tackle the same issue.&lt;br /&gt;
&lt;br /&gt;
We have recently been analysing how course creation is achieved. Two main ideas were tested with focus groups to try and find the best away to approach course creation. With this information we are confident that we have a user focused design that will improve the activity chooser for everyone.&lt;br /&gt;
&lt;br /&gt;
We would like to invite everyone to express their opinion on this improvement. Course creation is a Moodle activity that is fundamental to teaching a course online, and we would like to ensure that the process is as easy and intuitive as possible.&lt;br /&gt;
&lt;br /&gt;
== Features ==&lt;br /&gt;
&lt;br /&gt;
The following are changes that we are planning on making in this project. We have a demo that can be viewed and interacted with.&lt;br /&gt;
[https://projects.invisionapp.com/share/SVSREPYNBYG#/screens/388682478 Invisio mockup of the activity chooser].&lt;br /&gt;
&lt;br /&gt;
=== Larger display area ===&lt;br /&gt;
&lt;br /&gt;
The activity chooser will be wider and have the activities in a grid format. This allows for more activities to be seen at once.&lt;br /&gt;
&lt;br /&gt;
=== Activities and resources are now merged ===&lt;br /&gt;
&lt;br /&gt;
Our research found that the distinction been activities and resources was not useful to teachers and so now these two categories have been merged together.&lt;br /&gt;
&lt;br /&gt;
=== Starred / Favourites tab ===&lt;br /&gt;
&lt;br /&gt;
The user can now select activities to be added to the Starred tab. The starred tab is shown by default to users when pulling up the activity chooser.&lt;br /&gt;
&lt;br /&gt;
[[File:activity-chooser-starred.png|The starred tab]]&lt;br /&gt;
&lt;br /&gt;
=== Recommended tab ===&lt;br /&gt;
&lt;br /&gt;
Site administrators will now be able to set a selection of activities as recommended. These recommended activities will show up in a tab in the activity chooser for the course creator to view. If no recommendations are made then this tab will not be displayed.&lt;br /&gt;
&lt;br /&gt;
[[File:activity-chooser-recommend.png|The recommended tab]]&lt;br /&gt;
&lt;br /&gt;
=== H5P activities included ===&lt;br /&gt;
&lt;br /&gt;
We are looking at adding the ability for users to select an H5P activity from the activity chooser.&lt;br /&gt;
&lt;br /&gt;
=== Smart search bar ===&lt;br /&gt;
&lt;br /&gt;
To help find activities from the activity chooser, we will be adding a search bar, that will search through both the names of the activities, and also the information text, to try and find relevant activities that the user may want.&lt;br /&gt;
&lt;br /&gt;
=== Other activity types ===&lt;br /&gt;
&lt;br /&gt;
Other activity types such as LTI will be able to be added to the activity chooser for the user to select.&lt;br /&gt;
&lt;br /&gt;
=== Activity information hidden ===&lt;br /&gt;
&lt;br /&gt;
The information about an activity will be accessible through the &#039;i&#039; icon. Clicking the link will show additional information about the activity. This will free up space for other activities to be shown rather than always taking up half of the activity chooser.&lt;br /&gt;
&lt;br /&gt;
[[File:activity-chooser-info.png|Additional information about an activity]]&lt;br /&gt;
&lt;br /&gt;
=== Adding activity items to the chooser through plugins ===&lt;br /&gt;
&lt;br /&gt;
It is currently possible to include additional items to the activity picker (such as the external tools). We are looking at expanding this to allow any plugin to add items to the activity picker.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;What do you mean by this? I can see it makes sense for mod_ plugins to be able to do this, but why any plugin type? Can you give an example that shows why this is a requirement. Thanks. --Tim.&#039;&#039;&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_3.8_release_notes&amp;diff=56639</id>
		<title>Moodle 3.8 release notes</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_3.8_release_notes&amp;diff=56639"/>
		<updated>2019-11-13T23:33:13Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Releases]] &amp;gt; {{FULLPAGENAME}}&lt;br /&gt;
 &lt;br /&gt;
&lt;br /&gt;
Release date: Not yet released - scheduled for 18 November 2019&lt;br /&gt;
&lt;br /&gt;
Here is [https://tracker.moodle.org/secure/IssueNavigator!executeAdvanced.jspa?jqlQuery=project+%3D+mdl+AND+resolution+%3D+fixed+AND+fixVersion+in+%28%223.8%22%29+ORDER+BY+priority+DESC&amp;amp;runQuery=true&amp;amp;clear=true the full list of fixed issues in 3.8].&lt;br /&gt;
&lt;br /&gt;
If you are upgrading from a previous version, please see [[:en:Upgrading|Upgrading]] in the user docs.&lt;br /&gt;
&lt;br /&gt;
==Server requirements==&lt;br /&gt;
&lt;br /&gt;
These are just the minimum supported versions. We recommend keeping all of your software and operating systems up-to-date.&lt;br /&gt;
&lt;br /&gt;
* Moodle upgrade:  Moodle 3.2 or later&lt;br /&gt;
* PHP version: minimum PHP 7.1.0 &#039;&#039;Note: minimum PHP version has increased since Moodle 3.6&#039;&#039;. PHP 7.2.x and 7.3.x are supported too. PHP 7.x could have some [https://docs.moodle.org/dev/Moodle_and_PHP7#Can_I_use_PHP7_yet.3F engine limitations]. &lt;br /&gt;
* PHP extension &#039;&#039;&#039;intl&#039;&#039;&#039; is required since Moodle 3.4 (it was recommended in 2.0 onwards) &lt;br /&gt;
&lt;br /&gt;
=== Database requirements ===&lt;br /&gt;
&lt;br /&gt;
Moodle supports the following database servers. Again, version numbers are just the minimum supported version. We recommend running the latest stable version of any software.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;nicetable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Database&lt;br /&gt;
! Minimum version&lt;br /&gt;
! Recommended&lt;br /&gt;
|-&lt;br /&gt;
| [http://www.postgresql.org/ PostgreSQL]&lt;br /&gt;
| 9.4&lt;br /&gt;
| Latest&lt;br /&gt;
|-&lt;br /&gt;
| [http://www.mysql.com/ MySQL]&lt;br /&gt;
| 5.6&lt;br /&gt;
| Latest&lt;br /&gt;
|-&lt;br /&gt;
| [https://mariadb.org/ MariaDB]&lt;br /&gt;
| 5.5.31&lt;br /&gt;
| Latest&lt;br /&gt;
|-&lt;br /&gt;
| [http://www.microsoft.com/en-us/server-cloud/products/sql-server/ Microsoft SQL Server]&lt;br /&gt;
| 2012 &#039;&#039;(increased since Moodle 3.7)&#039;&#039;&lt;br /&gt;
| Latest&lt;br /&gt;
|-&lt;br /&gt;
| [http://www.oracle.com/us/products/database/overview/index.html Oracle Database]&lt;br /&gt;
| 11.2&lt;br /&gt;
| Latest&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Client requirements==&lt;br /&gt;
&lt;br /&gt;
=== Browser support ===&lt;br /&gt;
Moodle is compatible with any standards compliant web browser. We regularly test Moodle with the following browsers:&lt;br /&gt;
&lt;br /&gt;
Desktop:&lt;br /&gt;
* Chrome&lt;br /&gt;
* Firefox&lt;br /&gt;
* Safari&lt;br /&gt;
* Edge&lt;br /&gt;
* Internet Explorer&lt;br /&gt;
&lt;br /&gt;
Mobile:&lt;br /&gt;
* MobileSafari&lt;br /&gt;
* Google Chrome&lt;br /&gt;
&lt;br /&gt;
For the best experience and optimum security, we recommend that you keep your browser up to date.&lt;br /&gt;
&lt;br /&gt;
Note: Legacy browsers with known compatibility issues with Moodle 3.5:&lt;br /&gt;
* Internet Explorer 10 and below&lt;br /&gt;
* Safari 7 and below&lt;br /&gt;
 &lt;br /&gt;
&lt;br /&gt;
==Major features==&lt;br /&gt;
&lt;br /&gt;
===Analytics===&lt;br /&gt;
&lt;br /&gt;
* MDL-64739 - Analytics models may be restricted to category or course contexts&lt;br /&gt;
* MDL-65588 - Insights about students who have not logged in recently&lt;br /&gt;
* MDL-65562 - Report on the actions executed by users on predictions&lt;br /&gt;
* MDL-65633 - Allow targets to limit the analysis interval to a specific interface or parent class.&lt;br /&gt;
* MDL-66234 - Extra garbage collection for analytics&lt;br /&gt;
* MDL-66254 - Require enrolments to be active for most of the analysis interval&lt;br /&gt;
* MDL-62191 - Add bulk actions for analytics&#039; insights&lt;br /&gt;
* MDL-66536 - Insight notifications improvements&lt;br /&gt;
* MDL-60949 - Analytics models should be sorted by name and not last modified&lt;br /&gt;
* MDL-66004 - Allow the Python machine learning backend to run from a separate server&lt;br /&gt;
&lt;br /&gt;
===Forum summary report===&lt;br /&gt;
&lt;br /&gt;
* MDL-66298 - Forum summary report option to message selected users&lt;br /&gt;
* MDL-66153 - Forum report: Basic skeleton&lt;br /&gt;
* MDL-66268 - Groups filter in forum summary report&lt;br /&gt;
* MDL-66373 - Dates filter in forum summary report&lt;br /&gt;
* MDL-66297 - Link forum summary report to export of each user&#039;s post content&lt;br /&gt;
* MDL-66694 - Add columns for word count and character count to the forum summary report&lt;br /&gt;
* MDL-66768 - Add the ability to download the forum summary report&lt;br /&gt;
&lt;br /&gt;
===Forum export===&lt;br /&gt;
&lt;br /&gt;
* MDL-66075 - Forum export functionality&lt;br /&gt;
* MDL-66631 - Dates filter in forum export&lt;br /&gt;
* MDL-66808 - Forum export options for human-readable dates and removing HTML&lt;br /&gt;
&lt;br /&gt;
===Forum grading===&lt;br /&gt;
&lt;br /&gt;
* MDL-66074 - Create forum grading interface&lt;br /&gt;
* MDL-66358 - Display grading form in the grading panel&lt;br /&gt;
* MDL-66365 - Add a button to display the entire discussion for a post being graded&lt;br /&gt;
* MDL-67116 - Make &#039;require grade&#039; an activity completion criterion for the forum&lt;br /&gt;
* MDL-66381 - Forum grading user search&lt;br /&gt;
* MDL-66360 - Forum grading option to send notification to student&lt;br /&gt;
* MDL-66906 - Forum view grades option for students&lt;br /&gt;
* MDL-66359 - Support restricting the user list to a specific group&lt;br /&gt;
&lt;br /&gt;
===Forum UI improvements===&lt;br /&gt;
&lt;br /&gt;
* MDL-66477 - Create settings side drawer for new discussion view&lt;br /&gt;
* MDL-64821 - Create new discussion view for forum&lt;br /&gt;
* MDL-66481 - Update display of discussion in discussion list table&lt;br /&gt;
* MDL-65129 - Search starred discussions only option in forum advanced search&lt;br /&gt;
&lt;br /&gt;
===Question bank improvements===&lt;br /&gt;
&lt;br /&gt;
* MDL-66553 - Display ID number and tags in the question bank UI&lt;br /&gt;
* MDL-66816 - Question bank: replace the row of edit icons with an Edit menu&lt;br /&gt;
* MDL-67153 - Allow question types to add extra actions to the Question bank edit menu&lt;br /&gt;
&lt;br /&gt;
===H5P integration===&lt;br /&gt;
&lt;br /&gt;
* MDL-66388 - Create a new button in Atto to add H5P content in anywhere from hp5.com and h5p.org external URLs&lt;br /&gt;
* MDL-66398 - Improve H5P filter to allow internal H5P content URLs&lt;br /&gt;
* MDL-66593 - Implement backup and restore process for H5P content&lt;br /&gt;
* MDL-67059 - Add Admin UI to manually upload H5P content-type libraries&lt;br /&gt;
* MDL-67043 - Web service to enable H5P offline access in the Moodle app&lt;br /&gt;
* MDL-67057 - Create a capability to update H5P content-type libraries&lt;br /&gt;
* MDL-67058 - Create a task to install H5P content-type libraries&lt;br /&gt;
* MDL-66609 - Create the basic skeleton, library and interfaces for rendering H5P content&lt;br /&gt;
* MDL-66399 - Improve H5P Atto button to upload content&lt;br /&gt;
* MDL-66397 - Create a new filter to convert h5p.com and h5p.org URLs to embed code&lt;br /&gt;
&lt;br /&gt;
===Course relative dates (experimental)===&lt;br /&gt;
&lt;br /&gt;
* MDL-66147 - Assignment due date relative to the student course start date&lt;br /&gt;
* MDL-66144 - Weeks format relative dates&lt;br /&gt;
* MDL-66143 - Course relative dates mode setting&lt;br /&gt;
* MDL-66148 - Option to override the assignment due date in a relative dates course&lt;br /&gt;
&lt;br /&gt;
===Course overview===&lt;br /&gt;
&lt;br /&gt;
* MDL-64901 - block_myoverview: Add admin setting to control the available layouts&lt;br /&gt;
* MDL-66016 - An admin can set which filters are available for users to select in their Dashboard course overview&lt;br /&gt;
* MDL-66017 - An admin can specify a course custom field as a filter for users to select in their Dashboard course overview&lt;br /&gt;
* MDL-63612 - Course card pattern colours may be specified by an admin&lt;br /&gt;
* MDL-65621 - Courses with course visibility set to hide should be labelled &#039;Hidden from students&#039; in the course overview&lt;br /&gt;
* MDL-64860 - block_myoverview: Improve pagination widget&lt;br /&gt;
* MDL-64094 - Change &#039;Hidden&#039; to &#039;Removed from view&#039; in the course overview&lt;br /&gt;
&lt;br /&gt;
===Emojis===&lt;br /&gt;
&lt;br /&gt;
* MDL-65896 - Add emojis to messaging&lt;br /&gt;
* MDL-46779 - Atto should support full emoji&lt;br /&gt;
&lt;br /&gt;
==Other highlights==&lt;br /&gt;
&lt;br /&gt;
* MDL-65915 - Better progress display while re-grading quiz attempts&lt;br /&gt;
* MDL-66563 - Improve drag and drop question accessibility in high-contrast mode&lt;br /&gt;
&#039;&#039;Items to be added soon...&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==For admins==&lt;br /&gt;
&lt;br /&gt;
==For developers==&lt;br /&gt;
&lt;br /&gt;
* MDL-66675 - New &amp;lt;tt&amp;gt;$CFG-&amp;gt;behat_pause_on_fail&amp;lt;/tt&amp;gt; option added.&lt;br /&gt;
* MDL-46267 - The &amp;lt;tt&amp;gt;$CFG-&amp;gt;httpswwwroot&amp;lt;/tt&amp;gt; was removed.&lt;br /&gt;
* MDL-66335 - New steps to navigate straight to any plugin web page. Plugins must implement their own resolver between page types and URLs.&lt;br /&gt;
* MDL-65349 - Profiling included and excluded URLs now are matched from start. Some adjustments may be needed.&lt;br /&gt;
* MDL-66633 - Quiz: quiz attempt API should let you create an attempt for a different user&lt;br /&gt;
* MDL-66709 - Components other than activity modules should be able to backup and restore question attempt data&lt;br /&gt;
* MDL-66754 - Question engine: report methods should not require a list of slots&lt;br /&gt;
&lt;br /&gt;
==Security issues==&lt;br /&gt;
 &lt;br /&gt;
A number of security related issues were resolved. Details of these issues will be released after a period of approximately one week to allow system administrators to safely update to the latest version.&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
*[[Moodle 3.7 release notes]]&lt;br /&gt;
 &lt;br /&gt;
[[Category:Release notes]]&lt;br /&gt;
[[Category:Moodle 3.8]]&lt;br /&gt;
 &lt;br /&gt;
[[fr:Notes de mise à jour de Moodle 3.8]]&lt;br /&gt;
[[es:Notas de Moodle 3.8]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Availability_API&amp;diff=56114</id>
		<title>Availability API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Availability_API&amp;diff=56114"/>
		<updated>2019-06-04T13:11:36Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 2.7}}&lt;br /&gt;
&lt;br /&gt;
{{Infobox Project&lt;br /&gt;
|name = Availability API&lt;br /&gt;
|state = Integration review&lt;br /&gt;
|tracker = MDL-44070&lt;br /&gt;
|discussion = https://moodle.org/mod/forum/discuss.php?d=253958&lt;br /&gt;
|assignee = sam marshall (OU)&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
= Availability API =&lt;br /&gt;
&lt;br /&gt;
The availability API controls access to activities and sections. For example, a teacher could restrict access so that an activity cannot be accessed until a certain date, or so that a section cannot be accessed unless users have a certain grade in a quiz.&lt;br /&gt;
&lt;br /&gt;
The conditional availability system defaults to off; users enable it from the advanced features page in settings. You can still call the API functions even if the system is turned off.&lt;br /&gt;
&lt;br /&gt;
== Change from Moodle 2.6 and below ==&lt;br /&gt;
&lt;br /&gt;
This replaces the previous API (2.6 and below) in conditionlib.php. The main differences in the new code are:&lt;br /&gt;
&lt;br /&gt;
* Supports plugins for different types of condition.&lt;br /&gt;
* Allows conditions to be combined in Boolean logic tree structures.&lt;br /&gt;
&lt;br /&gt;
If you have code using conditionlib.php, you&#039;ll see deprecation warnings advising you how to modify your code.&lt;br /&gt;
&lt;br /&gt;
== Using the API ==&lt;br /&gt;
&lt;br /&gt;
In most cases you do not need to use the API directly because the system handles it. For example, if you are writing a module:&lt;br /&gt;
&lt;br /&gt;
* The system will automatically prevent users accessing your module if they do not meet an availability condition (unless they have the ability to access hidden activities). This happens when you call require_login and pass your module information.&lt;br /&gt;
* Your module&#039;s form will automatically have controls for setting availability restriction, as part of the standard form controls.&lt;br /&gt;
&lt;br /&gt;
However for special cases you might need to use API features. There are two main uses, shown below.&lt;br /&gt;
&lt;br /&gt;
=== Check if the current user can access an activity ===&lt;br /&gt;
&lt;br /&gt;
To check if the current user can access an activity (supposing you have a $course and $cmid):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$modinfo = get_fast_modinfo($course);&lt;br /&gt;
$cm = $modinfo-&amp;gt;get_cm($cmid);&lt;br /&gt;
if ($cm-&amp;gt;uservisible) {&lt;br /&gt;
    // User can access the activity.&lt;br /&gt;
} else if ($cm-&amp;gt;availableinfo) {&lt;br /&gt;
    // User cannot access the activity, but on the course page they will&lt;br /&gt;
    // see a link to it, greyed-out, with information (HTML format) from&lt;br /&gt;
    // $cm-&amp;gt;availableinfo about why they can&#039;t access it.&lt;br /&gt;
} else {&lt;br /&gt;
    // User cannot access the activity and they will not see it at all.&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are similar properties for sections.&lt;br /&gt;
&lt;br /&gt;
* You do not need to check the section properties when checking access for an activity, as the system takes these into account. (If the user cannot access a section, then $cm-&amp;gt;uservisible will return false for all activities within the section.)&lt;br /&gt;
* You do not need to additionally check $cm-&amp;gt;visible; $cm-&amp;gt;uservisible already takes this into account too.&lt;br /&gt;
&lt;br /&gt;
These properties are worked out for the current user. You can also obtain them for a different user by passing a user ID to get_fast_modinfo, although be aware that doing this repeatedly for different users will be slow.&lt;br /&gt;
&lt;br /&gt;
=== Display a list of users who may be able to access the current activity ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you need to display a list of users who may be able to access the current activity.&lt;br /&gt;
&lt;br /&gt;
While you could use the above approach for each user, this would be slow and also is generally not what you require. For example, if you have an activity such as an assignment which is set to be available to students until a certain date, and if you want to display a list of potential users within that activity, you probably don&#039;t want to make the list blank immediately the date occurs.&lt;br /&gt;
&lt;br /&gt;
The system divides availability conditions into two types: &lt;br /&gt;
&lt;br /&gt;
* Applied to user lists. (Group, grouping, profile condition.)&lt;br /&gt;
* Not applied to user lists. (Date, completion, grade.)&lt;br /&gt;
&lt;br /&gt;
In general, the conditions which we expect are likely to change over time (such as dates) or as a result of user actions (such as grades) are not applied to user lists.&lt;br /&gt;
&lt;br /&gt;
If you have a list of users (for example you could obtain this using one of the &#039;get enrolled users&#039; functions), you can filter it to include only those users who are allowed to see an activity with this code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$info = new \core_availability\info_module($cm);&lt;br /&gt;
$filtered = $info-&amp;gt;filter_user_list($users);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* This does not currently include the $cm-&amp;gt;visible setting, nor does it take into account the viewhiddenactivities setting.&lt;br /&gt;
&lt;br /&gt;
== Implementing new availability conditions ==&lt;br /&gt;
&lt;br /&gt;
It is easy to write new availability conditions. See [[Availability conditions]].&lt;br /&gt;
&lt;br /&gt;
== Using availability conditions in other areas ==&lt;br /&gt;
&lt;br /&gt;
The availability API is provided for activities (course-modules) and sections. It is also possible to use it in other areas such as within a module. See [[Availability API for items within a module]].&lt;br /&gt;
&lt;br /&gt;
== Programmatically setting availability conditions ==&lt;br /&gt;
&lt;br /&gt;
If you want to programmatically set up an availability condition (e.g. set an activity to only be available to users of a particular group, the best code to do that is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
$restriction = \core_availability\tree::get_root_json(&lt;br /&gt;
        [\availability_group\condition::get_json($group-&amp;gt;id)]);&lt;br /&gt;
$DB-&amp;gt;set_field(&#039;course_modules&#039;, &#039;availability&#039;,&lt;br /&gt;
        json_encode($restriction), [&#039;id&#039; =&amp;gt; $cmid]);&lt;br /&gt;
rebuild_course_cache($course-&amp;gt;id, true);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_acceptance_tests&amp;diff=56111</id>
		<title>Writing acceptance tests</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_acceptance_tests&amp;diff=56111"/>
		<updated>2019-06-03T16:04:54Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
This documentation gives some hints, how to write behat tests for core and for plugins. The focus of the documentation is on behat tests for plugins. They are written in some kind of natural language and describe how the front-end of moodle should behave, when a user interacts with it.&lt;br /&gt;
&lt;br /&gt;
Each test consists of a set of so called steps in a GIVEN, WHEN, THEN style:&lt;br /&gt;
* &#039;&#039;&#039;GIVEN&#039;&#039;&#039;: These steps outline the state of your moodle platform at the start of your test. Here you can create users, courses and plugin instances.&lt;br /&gt;
* &#039;&#039;&#039;WHEN&#039;&#039;&#039;: These steps usually execute the functionality of your plugin, which is under test.&lt;br /&gt;
* &#039;&#039;&#039;THEN&#039;&#039;&#039;: These steps describe the expected behaviour of your plugin. They usually check if different elements can or can&#039;t be seen.&lt;br /&gt;
This matches the standard [http://xunitpatterns.com/Four%20Phase%20Test.html Four-phase test pattern]. The fourth phase is &#039;tear-down&#039; which we don&#039;t need because Behat automatically cleans up after each test.&lt;br /&gt;
&lt;br /&gt;
To initialize and run your tests, please follow the instructions of [[Running_acceptance_test]].&lt;br /&gt;
&lt;br /&gt;
== Create your own tests ==&lt;br /&gt;
Behat tests are located within the directory tests/behat of your plugin.&lt;br /&gt;
The different tests are defined in files with the ending *.feature.&lt;br /&gt;
First, you have to define the header of your test:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@mod @mod_yourplugin @javascript&lt;br /&gt;
Feature: Here comes a description of your user story.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The tags on top of the feature description can be used to select specific test cases when running the tests.&lt;br /&gt;
The &#039;@javascript&#039; tag should only be used, if javascript is needed to execute your test. This is dependent on the step you will use in your definition.&lt;br /&gt;
Javascript tests are usually much slower than tests executed without javascript.&lt;br /&gt;
&lt;br /&gt;
Afterwards you can specify a scenario:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@javascript&lt;br /&gt;
  Scenario: Description of your scenario, which you want to test.&lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I am on &amp;quot;Course 1&amp;quot; course homepage&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Again you can define specific tags. Afterwards you write the steps, which should be executed during your test.&lt;br /&gt;
&lt;br /&gt;
==== Multiple Scenarios ====&lt;br /&gt;
You can have an arbitrary amount of scenarios within a test. Please make sure they all belong to the same feature.&lt;br /&gt;
If you have certain steps, which should be executed for every scenario of a feature, you can define them using a background:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname | category | groupmode |&lt;br /&gt;
      | Course 1 | C1        | 0        | 1         |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username | firstname | lastname | email |&lt;br /&gt;
      | teacher1 | Theo | Teacher | teacher1@example.com |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
This is usually used, to define the different &#039;&#039;&#039;GIVEN&#039;&#039;&#039; steps.&lt;br /&gt;
&lt;br /&gt;
==== Use existing steps ====&lt;br /&gt;
There are different ways how to effectively browse the available existing steps:&lt;br /&gt;
&lt;br /&gt;
====== Moodle Administration ======&lt;br /&gt;
Moodle offers within its administration menu under Site Administration &amp;gt; Development &amp;gt; Acceptance Testing a complete and searchable list of all available step definitions.&lt;br /&gt;
However, make sure you installed the behat test site first!&lt;br /&gt;
&lt;br /&gt;
====== PhpStorm ======&lt;br /&gt;
In PhpStorm or IntelliJ you can install the behat extension. Then you get auto completions within feature files, which helps a lot during behat test development.&lt;br /&gt;
&lt;br /&gt;
==== Providing values to steps ====&lt;br /&gt;
Most of the steps requires values, there are methods to provide values to steps, the method depends on the step specification.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;A string/text&#039;&#039;&#039;; is the most common case, the texts are wrapped between double quotes (&amp;quot; character) you have to replace the info about the expected value for your value; for example something like &#039;&#039;&#039;I press &amp;quot;BUTTON_STRING&amp;quot;&#039;&#039;&#039; should become &#039;&#039;&#039;I press &amp;quot;Save and return to course&amp;quot;&#039;&#039;&#039;. If you want to add a string which contains a &amp;quot; character, you can escape it with \&amp;quot;, for example &#039;&#039;&#039;I fill the &amp;quot;Name&amp;quot; field with &amp;quot;Alan alias \&amp;quot;the legend\&amp;quot;&amp;quot;&#039;&#039;&#039;. You can identify this steps because they ends with &#039;&#039;&#039;_STRING&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;A number&#039;&#039;&#039;; some steps requires numbers as values, to be more specific an undetermined number of digits from 0 to 9 (Natural numbers + 0) you can identify them because the expected value info string ends with &#039;&#039;&#039;_NUMBER&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;A table&#039;&#039;&#039;; is a relation between values, the most common use of it is to fill forms. The steps which requires tables are easily identifiable because they finish with &#039;&#039;&#039;:&#039;&#039;&#039; The steps description gives info about what the table columns must contain, for example &#039;&#039;&#039;Fills a moodle form with field/value data&#039;&#039;&#039;. Here you don&#039;t need to escape the double quotes if you want to include them as part of the value.&lt;br /&gt;
* &#039;&#039;&#039;A PyString&#039;&#039;&#039;; is a multiline string, most commonly used to fill out forms when a newline is required. Like steps with tables, steps which require PyStrings will end with &amp;quot;:&amp;quot;&lt;br /&gt;
* &#039;&#039;&#039;A field value&#039;&#039;&#039;; There are many different field types, if an argument requires a field value the expected value will depend on the field type:&lt;br /&gt;
** Text-based fields: It expects the text. This includes textareas, input type text, input type password...&lt;br /&gt;
** Checkbox: It expects 1 to check and for checked and &amp;quot;&amp;quot; to uncheck or for unchecked&lt;br /&gt;
** Select: It expects the option text or the option value. In case you interact with a multi-select you should specify the options separating them with commas. For example: &#039;&#039;&#039;option1, option2, option3&#039;&#039;&#039;&lt;br /&gt;
** Radio: The text of the radio option&lt;br /&gt;
* &#039;&#039;&#039;A selector&#039;&#039;&#039;; there are steps that can be used with different kinds of elements, for example &#039;&#039;&#039;I click on &amp;quot;User Name&amp;quot; &amp;quot;link&amp;quot;&#039;&#039;&#039; or &#039;&#039;&#039;I click on &amp;quot;User Name&amp;quot; &amp;quot;button&amp;quot;&#039;&#039;&#039; this is a closed list of elements, they always works together with another argument, where you specify the locator (eg. the link text in a link) In the &#039;Acceptance testing&#039; interface you can see a drop-down menu to select one of these options:&lt;br /&gt;
** field - for searching a field by its id, name, value or label&lt;br /&gt;
** link - for searching a link by its href, id, title, img alt or value&lt;br /&gt;
** button - for searching a button by its name, id, value, img alt or title&lt;br /&gt;
** link_or_button - for searching for both, links and buttons&lt;br /&gt;
** select - for searching a select field by its id, name or label&lt;br /&gt;
** checkbox - for searching a checkbox by its id, name, or label&lt;br /&gt;
** radio - for searching a radio button by its id, name, or label&lt;br /&gt;
** file - for searching a file input by its id, name, or label&lt;br /&gt;
** optgroup - for searching optgroup by its label&lt;br /&gt;
** option - for searching an option by its content&lt;br /&gt;
** dialogue - for searching a dialogue with the specified header text&lt;br /&gt;
** filemanager - for searching a filemanager by it&#039;s id or label&lt;br /&gt;
** block - for searching a Moodle block by it&#039;s English name or it&#039;s frankenstyle name&lt;br /&gt;
** section - for searching for a section on a course page by it&#039;s title or its written out date (e.g. &amp;quot;1 January - 7 January&amp;quot;). Use &amp;quot;frontpage&amp;quot; &amp;quot;section&amp;quot; for the frontpage section if it has no title (default)&lt;br /&gt;
** activity - for searching for an activity module in a course list by it&#039;s title&lt;br /&gt;
** region - for searching a Moodle page region with that id, in fact it works with all the page&#039;s ids&lt;br /&gt;
** table_row - for searching a table row which contains the specified text&lt;br /&gt;
** table - for searching a table by its id or caption&lt;br /&gt;
** fieldset - for searching a fieldset by it&#039;s id or legend&lt;br /&gt;
** css_element - for searching an element by its CSS selector&lt;br /&gt;
** xpath_element - for searching an element by its XPath&lt;br /&gt;
* &#039;&#039;&#039;A text selector&#039;&#039;&#039;; similar to a selector but those are the elements that returns an area of the DOM, they are useful in steps following the format &#039;&#039;&#039;... in the &amp;quot;Community finder&amp;quot; &amp;quot;block&amp;quot;&#039;&#039;&#039; where you are clicking or looking for some text inside a specific area. In the &#039;Acceptance testing&#039; interface you can see a drop-down menu to select one of these options:&lt;br /&gt;
** dialogue - for searching a dialogue with the specified header text&lt;br /&gt;
** block - for searching a Moodle block by it&#039;s English name or it&#039;s frankenstyle name&lt;br /&gt;
** section - for searching for a section on a course page by it&#039;s title or its written out date (e.g. &amp;quot;1 January - 7 January&amp;quot;). Use &amp;quot;frontpage&amp;quot; &amp;quot;section&amp;quot; for the frontpage section if it has no title (default)&lt;br /&gt;
** activity - for searching for an activity module in a course list by it&#039;s title&lt;br /&gt;
** region - for searching a Moodle page region with that id, in fact it works with all the page&#039;s ids&lt;br /&gt;
** table_row - for searching a table row which contains the specified text&lt;br /&gt;
** table - for searching a table by its id or caption&lt;br /&gt;
** fieldset - for searching a fieldset by it&#039;s id or legend&lt;br /&gt;
** css_element - for searching an element by its CSS selector&lt;br /&gt;
** xpath_element - for searching an element by its XPath&lt;br /&gt;
&lt;br /&gt;
====== Checking table values ======&lt;br /&gt;
You can check if specific value exists or not in a table row/column by using:&lt;br /&gt;
* Then &amp;quot;STRING_IN_ROW&amp;quot; row &amp;quot;COLUMN_HEADER&amp;quot; column of &amp;quot;TABLE_ID&amp;quot; table should contain &amp;quot;VALUE_TO_CHECK&amp;quot;&lt;br /&gt;
* Then the following should exist in the &amp;quot;TABLE_ID&amp;quot; table:&lt;br /&gt;
    | COLUMN_HEADER1 | COLUMN_HEADER2 |&lt;br /&gt;
    | VALUE_IN_ROW_1 | VALUE_IN_ROW_1 |&lt;br /&gt;
    | VALUE_IN_ROW_2 | VALUE_IN_ROW_2 |&lt;br /&gt;
&lt;br /&gt;
==== Advanced use cases ====&lt;br /&gt;
Most of the time the usage of existing step definitions is straight forward. However, there are some exceptions were it might get complicated. Some of them are listed here:&lt;br /&gt;
&lt;br /&gt;
====== Uploading files ======&lt;br /&gt;
Note than some tests requires files to be uploaded, in this case&lt;br /&gt;
* The &#039;&#039;&#039;I upload &amp;quot;FILEPATH_STRING&amp;quot; file to &amp;quot;FILEPICKER_FIELD_STRING&amp;quot; filepicker&#039;&#039;&#039; step can be used when located in the form page&lt;br /&gt;
* The file to upload should be included along with the Moodle codebase in COMPONENTNAME/tests/fixtures/*&lt;br /&gt;
* The file to upload is specified by it&#039;s path, which should be relative to the codebase root (&#039;&#039;&#039;lib/tests/fixtures/users.csv&#039;&#039;&#039; for example) &lt;br /&gt;
* &#039;&#039;&#039;/&#039;&#039;&#039; should be used as directory separator and the file names can not include this &#039;&#039;&#039;/&#039;&#039;&#039; character as all of them would be converted to the OS-dependant directory separator to maintain the compatibility with Windows systems.&lt;br /&gt;
* The scenarios that includes files uploading should be tagged using the &#039;&#039;&#039;@_file_upload&#039;&#039;&#039; tag&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@editor @editor_atto @atto @atto_media @_file_upload&lt;br /&gt;
Feature: Add media to Atto&lt;br /&gt;
  To write rich text - I need to add media.&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given I log in as &amp;quot;admin&amp;quot;&lt;br /&gt;
    And I follow &amp;quot;Manage private files...&amp;quot;&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.webm&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.mp4&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.png&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/pretty-good-en.vtt&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/pretty-good-sv.vtt&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I click on &amp;quot;Save changes&amp;quot; &amp;quot;button&amp;quot;&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Field groups ======&lt;br /&gt;
This section describes how you can use the step definitions&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
...&lt;br /&gt;
When I set the field &amp;quot;([^&amp;quot;]|\&amp;quot;*)&amp;quot; to &amp;quot;([^&amp;quot;]|\&amp;quot;*)&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
for field groups. Examples for such field groups are the duration field or the date_time_selector. These are not displayed as one single input field within the front-end but consist of multiple input fields within one row.&lt;br /&gt;
You can access each single input field of a group using &lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
identifierOfYourField[keyOfTheSpecificInput]&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Examples would be:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
  | myDate[day]             |   21   |&lt;br /&gt;
  | myDate[month]           |   12   |&lt;br /&gt;
  | myDate[hour]            |   14   |&lt;br /&gt;
  | myDuration[number]      |   10   |&lt;br /&gt;
  | myDuration[unit]        | days   |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Human-readable and relative dates ======&lt;br /&gt;
When testing plugins with deadlines, for instance for submissions, it is often necessary to set certain time values to dates relative to today.&lt;br /&gt;
You can specify a relative time enclosed within two ## blocks. For example:&lt;br /&gt;
* ## yesterday ##&lt;br /&gt;
* ## 2 days ago ##&lt;br /&gt;
You can use everything according to [http://php.net/manual/en/datetime.formats.php].&lt;br /&gt;
&lt;br /&gt;
Especially useful are the relative formats from: [http://php.net/manual/en/datetime.formats.relative.php]&lt;br /&gt;
&lt;br /&gt;
Additionally, you can specify a format you want the date to be returned into:&lt;br /&gt;
* ## yesterday ## myformat ##&lt;br /&gt;
These formats can be used as outlined in [http://php.net/manual/en/function.date.php].&lt;br /&gt;
This can be combined with the field groups:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
  | myDate[day]             |   ## yesterday ## j ##   |&lt;br /&gt;
  | myDate[month]           |   ## yesterday ## n ##   |&lt;br /&gt;
  | myDate[year]            |   ## yesterday ## Y ##   |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Good practice ==&lt;br /&gt;
&lt;br /&gt;
=== Test one thing per scenario ===&lt;br /&gt;
&lt;br /&gt;
The ideal that you should strive for, is that each scenario tests just one specific bit of functionality. Therefore, if one test fails, the scenario name should tell you exactly what the bug is. Also, any bug should cause just one scenario to fail, not lots of unrelated ones. If you can achieve this, then the idea is that it minimises the time from seeing a test fail to having fixed the bug that was detected. Of course, this ideal is not always achievable, but in my experience it is worth striving for.&lt;br /&gt;
&lt;br /&gt;
=== Set-up (Given) should not use the UI ===&lt;br /&gt;
&lt;br /&gt;
The setup is not what you are really testing here. Therefore, it should be as quick and reliable as possible. The way to achieve this is with steps like &amp;lt;tt&amp;gt;And the following &amp;quot;Thing&amp;quot; exist:&amp;lt;/tt&amp;gt; which directly insert the data into the database. If necessary, write extra steps for your plugin to setup the things you need.&lt;br /&gt;
&lt;br /&gt;
=== Don&#039;t use XPath or CSS selectors - fix your Accessibility bugs ===&lt;br /&gt;
&lt;br /&gt;
If, the only way you can identify something in the page that you want to manipulate is with a step like &amp;lt;tt&amp;gt;I set the field with xpath &amp;quot;//textarea[contains(@name, &#039;answer&#039;)]&amp;quot; to &amp;quot;frog&amp;quot;&amp;lt;/tt&amp;gt;, then this is probably the sign that you have an Accessibility bug, because Behat accesses the page very like a screen-reader user would.&lt;br /&gt;
&lt;br /&gt;
You should be able to refer to things with steps like &amp;lt;tt&amp;gt;I set the field &amp;quot;Answer&amp;quot; to &amp;quot;frog&amp;quot;&#039;&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I click on &amp;quot;True&amp;quot; &amp;quot;radio&amp;quot; in the &amp;quot;First question&amp;quot; &amp;quot;question&amp;quot;&amp;lt;/tt&amp;gt;. If not, you should probably think about fixing the accessibility bug, rather than resorting to unreadable selectors in your Behat test.&lt;br /&gt;
&lt;br /&gt;
=== When you define more steps in your plugin, make it clear they come from your plugin ===&lt;br /&gt;
&lt;br /&gt;
When defining new Step definitions in your plugin, try to make sure the step name identifies it as belonging to your plugin. So, don&#039;t make a step called &amp;lt;tt&amp;gt;I disable UI plugins&amp;lt;/tt&amp;gt;. Call it something like &amp;lt;tt&amp;gt;I disable UI plugins in the CodeRunner question type&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing&amp;diff=55978</id>
		<title>Acceptance testing</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing&amp;diff=55978"/>
		<updated>2019-04-25T20:32:24Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Moodle uses a framework called Behat to automatically test the user-interface. Tests can be written for each plugin, and for Moodle core.&lt;br /&gt;
&lt;br /&gt;
* To run the existing tests, read [[Running acceptance test]]. You really need to do this first.&lt;br /&gt;
* To write new tests, read [[Writing acceptance tests]].&lt;br /&gt;
* To define new steps that can you used when writing tests, see [[Writing new acceptance test step definitions]]&lt;br /&gt;
&lt;br /&gt;
Because Behat tests work through the Moodle user interface, the are a bit slow. Therefore, you should probably also use [[PHPUnit]] to test the detailed edge cases in your code.&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_step_step_definitions&amp;diff=55977</id>
		<title>Writing new acceptance step step definitions</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_step_step_definitions&amp;diff=55977"/>
		<updated>2019-04-25T20:27:45Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: TimHunt moved page Writing new acceptance step step definitions to Writing new acceptance test step definitions&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#REDIRECT [[Writing new acceptance test step definitions]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_test_step_definitions&amp;diff=55976</id>
		<title>Writing new acceptance test step definitions</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_test_step_definitions&amp;diff=55976"/>
		<updated>2019-04-25T20:27:45Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: TimHunt moved page Writing new acceptance step step definitions to Writing new acceptance test step definitions&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;As well as using the already existing steps described in [[Writing acceptance tests]], you can also define new steps.&lt;br /&gt;
&lt;br /&gt;
This is most easily learned by looking at the examples that are already in the code. In any plugin, for example qtype_ddwtos, look at the file tests/behat/behat_qtype_ddwtos.php inside that plugin. Steps are defined by a function that has a special @Given, @When or @Then annotation in the PHPdoc comment. This gives a regular expression. Any step in a *.feature file which matches that regular expression will be translated into a call to that function.&lt;br /&gt;
&lt;br /&gt;
In terms of making the Behat test work, it does not matter whether you use @Given, @When or @Then. However, to make your step understandable to people using your step, it is important to use the right word. Use @Given for steps that set things up, @When for steps that perform actions, and @Then for steps that verify what happened.&lt;br /&gt;
&lt;br /&gt;
When defining new Step definitions in your plugin, try to make sure the step name identifies it as belonging to your plugin. So, don&#039;t make a step called &amp;lt;tt&amp;gt;I disable UI plugins&amp;lt;/tt&amp;gt;. Call it something like &amp;lt;tt&amp;gt;I disable UI plugins in the CodeRunner question type&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==See aslo==&lt;br /&gt;
&lt;br /&gt;
* Behat project documentation for this: http://behat.org/en/latest/user_guide/context/definitions.html&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_test_step_definitions&amp;diff=55975</id>
		<title>Writing new acceptance test step definitions</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_test_step_definitions&amp;diff=55975"/>
		<updated>2019-04-25T20:27:34Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;As well as using the already existing steps described in [[Writing acceptance tests]], you can also define new steps.&lt;br /&gt;
&lt;br /&gt;
This is most easily learned by looking at the examples that are already in the code. In any plugin, for example qtype_ddwtos, look at the file tests/behat/behat_qtype_ddwtos.php inside that plugin. Steps are defined by a function that has a special @Given, @When or @Then annotation in the PHPdoc comment. This gives a regular expression. Any step in a *.feature file which matches that regular expression will be translated into a call to that function.&lt;br /&gt;
&lt;br /&gt;
In terms of making the Behat test work, it does not matter whether you use @Given, @When or @Then. However, to make your step understandable to people using your step, it is important to use the right word. Use @Given for steps that set things up, @When for steps that perform actions, and @Then for steps that verify what happened.&lt;br /&gt;
&lt;br /&gt;
When defining new Step definitions in your plugin, try to make sure the step name identifies it as belonging to your plugin. So, don&#039;t make a step called &amp;lt;tt&amp;gt;I disable UI plugins&amp;lt;/tt&amp;gt;. Call it something like &amp;lt;tt&amp;gt;I disable UI plugins in the CodeRunner question type&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==See aslo==&lt;br /&gt;
&lt;br /&gt;
* Behat project documentation for this: http://behat.org/en/latest/user_guide/context/definitions.html&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_test_step_definitions&amp;diff=55974</id>
		<title>Writing new acceptance test step definitions</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_new_acceptance_test_step_definitions&amp;diff=55974"/>
		<updated>2019-04-25T20:19:43Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: TimHunt moved page Running acceptance test adding steps definitions to Writing new acceptance step step definitions without leaving a redirect&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#REDIRECT [[Writing_acceptance_tests]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Privacy_API&amp;diff=55968</id>
		<title>Privacy API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Privacy_API&amp;diff=55968"/>
		<updated>2019-04-24T11:14:36Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Retrieving the users in a context */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The [https://en.wikipedia.org/wiki/General_Data_Protection_Regulation General Data Protection Regulation] (GDPR) is an EU directive that looks at providing users with more control over their data and how it is processed. This regulation will come into effect on 25th of May 2018 and covers any citizen or permanent resident of the European Union. The directive will be respected by a number of other countries outside of the European Union.&lt;br /&gt;
&lt;br /&gt;
To help institutions become compliant with this new regulation we are adding functionality to Moodle. This includes a number of components, amongst others these include a user’s right to:&lt;br /&gt;
&lt;br /&gt;
* request information on the types of personal data held, the instances of that data, and the deletion policy for each;&lt;br /&gt;
* access all of their data; and&lt;br /&gt;
* be forgotten.&lt;br /&gt;
&lt;br /&gt;
The compliance requirements also extend to installed plugins (including third party plugins). These need to also be able to report what information they store or process regarding users, and have the ability to provide and delete data for a user request.&lt;br /&gt;
&lt;br /&gt;
This document describes the proposed API changes required for plugins which will allow a Moodle installation to become GDPR compliant.&lt;br /&gt;
&lt;br /&gt;
Target Audience: The intended audience for this document is Moodle plugin developers, who are aiming to ensure their plugins are updated to comply with GDPR requirements coming into effect in the EU in May, 2018.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Personal data in Moodle==&lt;br /&gt;
&lt;br /&gt;
From the GDPR Spec, Article 4:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;‘personal data’ means any information relating to an identified or identifiable natural person (‘data subject’); an identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In Moodle, we need to consider two main types of personal data; information entered by the user and information stored about the user. The key difference being that information stored about the user will have come from a source other than the user themselves. Both types of data can be used to form a profile of the individual.&lt;br /&gt;
&lt;br /&gt;
The most obvious clue to finding personal data entered by the user is the presence of a userid on a database field. Any data on the record (or linked records) pertaining to that user may be deemed personal data for that user, including things like timestamps and record identification numbers. Additionally, any free text field which allows the user to enter information must also be considered to be the personal data of that user.&lt;br /&gt;
&lt;br /&gt;
Data stored about the user includes things like ratings and comments made on a student submission. These may have been made by an assessor or teacher, but are considered the personal data of the student, as they are considered a reflection of the user’s competency in the subject matter and can be used to form a profile of that individual. &lt;br /&gt;
&lt;br /&gt;
The sections that follow outline what you need to do as a plugin developer to ensure any personal data is advertised and can be accessed and deleted according to the GDPR requirements.&lt;br /&gt;
&lt;br /&gt;
==Background==&lt;br /&gt;
&lt;br /&gt;
===Architecture overview===&lt;br /&gt;
&lt;br /&gt;
[[File:MoodlePrivacyMetadataUML.png|thumb|UML diagram of the metadata part of the privacy subsystem]]&lt;br /&gt;
[[File:MoodlePrivacyRequestUML.png|thumb|UML diagram of the request providers part of the privacy subsystem]]&lt;br /&gt;
&lt;br /&gt;
A new system for Privacy has been created within Moodle. This is broken down into several main parts and forms the &#039;&#039;core_privacy&#039;&#039; subsystem:&lt;br /&gt;
&lt;br /&gt;
* Some metadata providers - a set of PHP interfaces to be implemented by components for that component to describe the kind of data that it stores, and the purpose for its storage;&lt;br /&gt;
* Some request providers - a set of PHP interfaces to be implemented by components to allow that component to act upon user requests such as the Right to be Forgotten, and a Subject Access Request; and&lt;br /&gt;
* A manager - a concrete class used to bridge components which implement the providers with tools which request their data.&lt;br /&gt;
&lt;br /&gt;
All plugins will implement one metadata provider, and zero, one or two request providers.&lt;br /&gt;
&lt;br /&gt;
The &#039;request&#039; providers are responsible for several separate areas:&lt;br /&gt;
&lt;br /&gt;
# Detecting in which Moodle contexts a specific user has any personal data;&lt;br /&gt;
# Exporting all personal data from each of those contexts for that user;&lt;br /&gt;
# Deleting all personal data from each of those contexts for that user;&lt;br /&gt;
# Detecting which users have personal data in a specific Moodle context;&lt;br /&gt;
# Deleting all personal data for each of those users in that context; and&lt;br /&gt;
# Deleting all personal data for all users in a specific context.&lt;br /&gt;
&lt;br /&gt;
The export and delete phases use data from the detection phase, which allows for the possibility to exclude all data from certain contexts, as required.&lt;br /&gt;
&lt;br /&gt;
Please refer to the inline phpdocs of the [https://github.com/moodle/moodle/blob/v3.5.0/privacy/classes/manager.php#L31 core_privacy::manager class] for detailed description of the interfaces, their hierarchy and meaning.&lt;br /&gt;
&lt;br /&gt;
Please note that support for locating and removing multiple users in a single context was added in MDL-62560 for Moodle 3.6, 3.5.3, and 3.4.6.&lt;br /&gt;
This functionality has been added to allow support for removal of user data for users subject to speific role criterion, and to support expiry of the same data by role too.&lt;br /&gt;
&lt;br /&gt;
===Implementing a provider===&lt;br /&gt;
&lt;br /&gt;
All plugins will need to create a concrete class which implements the relevant metadata and request providers. The exact providers you need to implement will depend on what data you store, and the type of plugin. This is covered in more detail in the following sections of the document.&lt;br /&gt;
&lt;br /&gt;
In order to do so:&lt;br /&gt;
&lt;br /&gt;
# You must create a class called &#039;&#039;provider&#039;&#039; within the namespace &#039;&#039;\your_pluginname\privacy&#039;&#039;.&lt;br /&gt;
# This class must be created at &#039;&#039;path/to/your/plugin/classes/privacy/provider.php&#039;&#039;.&lt;br /&gt;
# You must have your class implement the relevant metadata and request interfaces.&lt;br /&gt;
&lt;br /&gt;
==Plugins which do not store personal data==&lt;br /&gt;
&lt;br /&gt;
Many Moodle plugins do not store any personal data. This is usually the case for plugins which just add functionality, or which display the data already stored elsewhere in Moodle.&lt;br /&gt;
&lt;br /&gt;
Some examples of plugin types which might fit this criteria include themes, blocks, filters, editor plugins, etc.&lt;br /&gt;
&lt;br /&gt;
Plugins which cause data to be stored elsewhere in Moodle (e.g. via a subsystem call) are considered to store data.&lt;br /&gt;
&lt;br /&gt;
One examples of a plugin which does not store any data would be the Calendar month block which just displays a view of the user’s calendar. It does not store any data itself.&lt;br /&gt;
&lt;br /&gt;
An example of a plugin which must not use the null provider is the Comments block. The comments block is responsible for data subsequently being stored within Moodle. Although the block doesn’t store anything itself, it interacts with the comments subsystem and is the only component which knows how that data maps to a user.&lt;br /&gt;
&lt;br /&gt;
===Implementation requirements===&lt;br /&gt;
&lt;br /&gt;
In order to let Moodle know that you have audited your plugin, and that you do not store any personal user data, you must implement the &#039;&#039;\core_privacy\local\metadata\null_provider&#039;&#039; interface in your plugin’s provider.&lt;br /&gt;
&lt;br /&gt;
These null providers can only be implemented where a plugin has:&lt;br /&gt;
&lt;br /&gt;
* no external links (e.g. sends data to an external service like an LTI provider, repository plugin which you can search on)&lt;br /&gt;
* no database tables which store user data (including IP addresses)&lt;br /&gt;
* no user preferences&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;null_provider&#039;&#039; requires you to define one function &#039;&#039;get_reason()&#039;&#039; which returns the language string identifier within your component.&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;blocks/calendar_month/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
 &amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
namespace block_calendar_month\privacy;&lt;br /&gt;
&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin does not store any personal user data.&lt;br /&gt;
    \core_privacy\local\metadata\null_provider {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the language string identifier with the component&#039;s language&lt;br /&gt;
     * file to explain why this plugin stores no data.&lt;br /&gt;
     *&lt;br /&gt;
     * @return  string&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_reason() : string {&lt;br /&gt;
        return &#039;privacy:metadata&#039;;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;blocks/calendar_month/lang/en/block_calendar_month.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
...&lt;br /&gt;
$string[&#039;privacy:metadata&#039;] = &#039;The Calendar block only displays existing calendar data.&#039;;&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That’s it. Congratulations, your plugin now implements the Privacy API.&lt;br /&gt;
&lt;br /&gt;
==Plugins which store personal data==&lt;br /&gt;
&lt;br /&gt;
Many Moodle plugins do store some form of personal data.&lt;br /&gt;
&lt;br /&gt;
In some cases this will be stored within database tables in your plugin, and in other cases this will be in one of Moodle’s core subsystems - for example your plugin may store files, ratings, comments, or tags.&lt;br /&gt;
&lt;br /&gt;
Plugins which do store data will need to:&lt;br /&gt;
&lt;br /&gt;
* Describe the type of data that they store;&lt;br /&gt;
* Provide a way to export that data; and&lt;br /&gt;
* Provide a way to delete that data.&lt;br /&gt;
&lt;br /&gt;
Data is described via a &#039;&#039;metadata&#039;&#039; provider, and it is both exported and deleted via an implementation of a &#039;&#039;request&#039;&#039; provider.&lt;br /&gt;
&lt;br /&gt;
These are both explained in the sections below.&lt;br /&gt;
&lt;br /&gt;
===Describing the type of data you store===&lt;br /&gt;
&lt;br /&gt;
In order to describe the type of data that you store, you must implement the &#039;&#039;\core_privacy\local\metadata\provider&#039;&#039; interface.&lt;br /&gt;
&lt;br /&gt;
This interfaces requires that you define one function: &#039;&#039;get_metadata&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There are several types of item to describe the data that you store. These are for:&lt;br /&gt;
&lt;br /&gt;
* Items in the Moodle database;&lt;br /&gt;
* Items stored by you in a Moodle subsystem - for example files, and ratings; and&lt;br /&gt;
* User preferences stored site-wide within Moodle for your plugin&lt;br /&gt;
&lt;br /&gt;
Note: All fields should include a description from a language string within your plugin.&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
namespace mod_forum\privacy;&lt;br /&gt;
use core_privacy\local\metadata\collection;&lt;br /&gt;
&lt;br /&gt;
class provider implements &lt;br /&gt;
        // This plugin does store personal user data.&lt;br /&gt;
        \core_privacy\local\metadata\provider {&lt;br /&gt;
&lt;br /&gt;
    public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
        // Here you will add more items into the collection.&lt;br /&gt;
&lt;br /&gt;
        return $collection;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Indicating that you store content in a Moodle subsystem====&lt;br /&gt;
&lt;br /&gt;
Many plugins will use one of the core Moodle subsystems to store data.&lt;br /&gt;
&lt;br /&gt;
As a plugin developer we do not expect you to describe those subsystems in detail, but we do need to know that you use them and to know what you use them for.&lt;br /&gt;
&lt;br /&gt;
You can indicate this by calling the &#039;&#039;add_subsystem_link()&#039;&#039; method on the &#039;&#039;collection&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=====Relevant subsystems=====&lt;br /&gt;
&lt;br /&gt;
You are likely to need to indicate use of the following subsystems that store user data:&lt;br /&gt;
&lt;br /&gt;
* Ratings (if users are allowed to rate items within your plugin)&lt;br /&gt;
* Tags (if users can tag items within your plugin)&lt;br /&gt;
* Comments (if users can make comments on items in your plugin)&lt;br /&gt;
* Questions (if the plugin uses core question types)&lt;br /&gt;
* Filesystem (if users can attach files to items within your plugin)&lt;br /&gt;
* ...?&lt;br /&gt;
&lt;br /&gt;
Some subsystems which store user data do not need to be listed:&lt;br /&gt;
&lt;br /&gt;
* (TBC - how about global search? Nothing lists it that I could see.)&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_subsystem_link(&lt;br /&gt;
        &#039;core_files&#039;,&lt;br /&gt;
        [],&lt;br /&gt;
        &#039;privacy:metadata:core_files&#039;&lt;br /&gt;
    );&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/lang/en/forum.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:core_files&#039;] = &#039;The forum stores files which have been uploaded by the user to form part of a forum post.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Describing data stored in database tables====&lt;br /&gt;
&lt;br /&gt;
Most Moodle plugins will store some form of user data in their own database tables.&lt;br /&gt;
&lt;br /&gt;
As a plugin developer you will need to describe each database table, and each field which includes user data.&lt;br /&gt;
&lt;br /&gt;
It is a matter of judgement which fields contain user data and which don&#039;t. Anything entered by, or directly about, the user probably counts as user data but it may be useful to include additional fields that explain the context of the data.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_database_table(&lt;br /&gt;
        &#039;forum_discussion_subs&#039;,&lt;br /&gt;
         [&lt;br /&gt;
            &#039;userid&#039; =&amp;gt; &#039;privacy:metadata:forum_discussion_subs:userid&#039;,&lt;br /&gt;
            &#039;discussionid&#039; =&amp;gt; &#039;privacy:metadata:forum_discussion_subs:discussionid&#039;,&lt;br /&gt;
            &#039;preference&#039; =&amp;gt; &#039;privacy:metadata:forum_discussion_subs:preference&#039;,&lt;br /&gt;
&lt;br /&gt;
         ],&lt;br /&gt;
        &#039;privacy:metadata:forum_discussion_subs&#039;&lt;br /&gt;
    );&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/lang/en/forum.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs&#039;] = &#039;Information about the subscriptions to individual forum discussions. This includes when a user has chosen to subscribe to a discussion, or to unsubscribe from one where they would otherwise be subscribed.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs:userid&#039;] = &#039;The ID of the user with this subscription preference.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs:discussionid&#039;] = &#039;The ID of the discussion that was subscribed to.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs:preference&#039;] = &#039;The start time of the subscription.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Indicating that you store site-wide user preferences====&lt;br /&gt;
&lt;br /&gt;
Many plugins will include one or more user preferences. Unfortunately this is one of Moodle’s older components and many of the values stored are not pure user preferences. Each plugin should be aware of how it handles its own preferences and is best placed to determine whether they are site-wide preferences, or per-instance preferences.&lt;br /&gt;
&lt;br /&gt;
Whilst most of these will have a fixed name (e.g. &#039;&#039;filepicker_recentrepository&#039;&#039;), some will include a variable of some kind (e.g. &#039;&#039;tool_usertours_tour_completion_time_2&#039;&#039;). Only the general name (in this case &amp;quot;tool_usertours_tour_completion_time_&amp;quot;) needs to be indicated, rather than one copy for each possible value of the variable.&lt;br /&gt;
&lt;br /&gt;
Also, these should only be &#039;&#039;site-wide&#039;&#039; user preferences which do not belong to a specific Moodle context.&lt;br /&gt;
&lt;br /&gt;
In the above examples:&lt;br /&gt;
&lt;br /&gt;
* Preference &#039;&#039;filepicker_recentrepository&#039;&#039; belongs to the file subsystem, and is a site-wide preference affecting the user anywhere that they view the filepicker.&lt;br /&gt;
* Preference &#039;&#039;tool_usertours_tour_completion_time_2&#039;&#039; belongs to user tours. User tours are a site-wide feature which can affect many parts of Moodle and cross multiple contexts.&lt;br /&gt;
&lt;br /&gt;
In some cases a value may be stored in the preferences table but is known to belong to a specific context within Moodle. In these cases they should be stored as metadata against that context rather than as a site-wide user preference.&lt;br /&gt;
&lt;br /&gt;
You can indicate this by calling the &#039;&#039;add_user_preference()&#039;&#039; method on the &#039;&#039;collection&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Any plugin providing user preferences must also implement the &#039;&#039;\core_privacy\local\request\user_preference_provider&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;admin/tool/usertours/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_user_preference(&#039;tool_usertours_tour_completion_time&#039;,&lt;br /&gt;
        &#039;privacy:metadata:preference:tool_usertours_tour_completion_time&#039;);&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;admin/tool/usertours/lang/en/tool_usertours.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:tool_usertours_tour_completion_time&#039;] = &#039;The time that a specific user tour was last completed by a user.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Indicating that you export data to an external location====&lt;br /&gt;
&lt;br /&gt;
Many plugins will interact with external systems - for example cloud-based services. Often this external location is configurable within the plugin either at the site or the instance level.&lt;br /&gt;
&lt;br /&gt;
As a plugin developer you will need to describe each &#039;&#039;type&#039;&#039; of target destination, alongside a list of each exported field which includes user data.&lt;br /&gt;
The &#039;&#039;actual&#039;&#039; destination does not need to be described as this can change based on configuration.&lt;br /&gt;
&lt;br /&gt;
You can indicate this by calling the &#039;&#039;add_external_location_link()&#039;&#039; method on the collection.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/lti/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_external_location_link(&#039;lti_client&#039;, [&lt;br /&gt;
            &#039;userid&#039; =&amp;gt; &#039;privacy:metadata:lti_client:userid&#039;,&lt;br /&gt;
            &#039;fullname&#039; =&amp;gt; &#039;privacy:metadata:lti_client:fullname&#039;,&lt;br /&gt;
        ], &#039;privacy:metadata:lti_client&#039;);&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/lti/lang/en/lti.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:lti_client&#039;] = &#039;In order to integrate with a remote LTI service, user data needs to be exchanged with that service.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:lti_client:userid&#039;] = &#039;The userid is sent from Moodle to allow you to access your data on the remote system.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:lti_client:fullname&#039;] = &#039;Your full name is sent to the remote system to allow a better user experience.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Providing a way to export user data===&lt;br /&gt;
&lt;br /&gt;
In order to export the user data that you store, you must implement the relevant request provider.&lt;br /&gt;
&lt;br /&gt;
We have named these request providers because they are called in response to a specific request from a user to access their information, however they also deal with the removal of user data following it&#039;s expiry.&lt;br /&gt;
&lt;br /&gt;
There are several different types of request provider, and you may need to implement several of these, depending on the type and nature of your plugin.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking plugins will fit into one of the following categories:&lt;br /&gt;
&lt;br /&gt;
* Plugins which are a subplugin of another plugin. Examples include &#039;&#039;assignsubmission&#039;&#039;, &#039;&#039;atto&#039;&#039;, and &#039;&#039;datafield&#039;&#039;;&lt;br /&gt;
* Plugins which are typically called by a Moodle subsystem. Examples include &#039;&#039;qtype&#039;&#039;, and &#039;&#039;profilefield&#039;&#039;;&lt;br /&gt;
* All other plugins which store data.&lt;br /&gt;
&lt;br /&gt;
Most plugins will fit into this final category, whilst other plugins may fall into several categories.&lt;br /&gt;
Plugins which &#039;&#039;define&#039;&#039; a subplugin will also be responsible for  collecting this data from their subplugins.&lt;br /&gt;
&lt;br /&gt;
A final category exists - plugins which store user preferences. In some cases this may be the &#039;&#039;only&#039;&#039; provider implemented.&lt;br /&gt;
&lt;br /&gt;
====Standard plugins which store data====&lt;br /&gt;
&lt;br /&gt;
A majority of Moodle plugins will fit into this category and will be required to implement the &#039;&#039;\core_privacy\local\request\plugin\provider&#039;&#039; interface. This interface requires that you define four functions (the first two of which are dealt with in this section):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;get_contexts_for_userid&#039;&#039; - to explain where data is held within Moodle for your plugin; and&lt;br /&gt;
* &#039;&#039;export_user_data&#039;&#039; - to export a user’s personal data from your plugin.&lt;br /&gt;
* &#039;&#039;delete_data_for_all_users_in_context&#039;&#039; - to delete all data for all users in the specified context.&lt;br /&gt;
* &#039;&#039;delete_data_for_user&#039;&#039; - to delete all user data for the specified user, in the specified contexts.&lt;br /&gt;
&lt;br /&gt;
These APIs make use of the Moodle &#039;&#039;context&#039;&#039; system to hierarchically store this data.&lt;br /&gt;
&lt;br /&gt;
In addition to these requirements, since Moodle 3.4.6, 3.5.3, any plugin which implements the plugin provider interface must also implement the &#039;&#039;\core_privacy\local\request\core_userlist_provider&#039;&#039; provider and implement functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;get_users_in_context&#039;&#039; - to locate the users who hold any personal data in a specific context; and&lt;br /&gt;
* &#039;&#039;delete_data_for_users&#039;&#039; - to delete data for multiple users in the specified context.&lt;br /&gt;
&lt;br /&gt;
A polyfill has not been provided for this new interface, but we recognise that developers who are maintaining older versions (Moodle 3.3) will hit problems with this new interface not existing. We recommend doing something similar to the following example.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
namespace assignsubmission_example\privacy;&lt;br /&gt;
&lt;br /&gt;
if (interface_exists(&#039;\core_privacy\local\request\userlist&#039;)) {&lt;br /&gt;
    interface my_userlist extends \core_privacy\local\request\userlist{}&lt;br /&gt;
} else {&lt;br /&gt;
    interface my_userlist {};&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
class provider implements my_userlist {&lt;br /&gt;
    &lt;br /&gt;
    /**&lt;br /&gt;
     * Get the list of users who have data within a context.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_users_in_context(userlist $userlist) {&lt;br /&gt;
        &lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Delete multiple users within a single context.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   approved_userlist       $userlist The approved context and user information to delete information for.&lt;br /&gt;
     */&lt;br /&gt;
    public static function delete_data_for_users(approved_userlist $userlist) {&lt;br /&gt;
&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Retrieving the list of contexts=====&lt;br /&gt;
&lt;br /&gt;
You are required to return the list of contexts for which the plugin stores data about the user. These are the standard Moodle contexts - CONTEXT_COURSE, CONTEXT_USER, CONTEXT_MODULE and so on. In many cases the link between the module type and the context is self-evident (e.g. activity modules). In some cases it may be less so. For example, an enrolment plugin (that stores user data, which most don&#039;t) would link to course context (users enrol in courses). Other types of plugins may be less obvious but you need to pick something. One way might to consider what context you would use when checking role capabilities. Consider that this will be used to structure the exported data and define what data is deleted when a context is expired. &lt;br /&gt;
&lt;br /&gt;
Contexts are retrieved using the &#039;&#039;get_contexts_for_userid&#039;&#039; function which takes the ID of the user being fetched, and returns a list of contexts in which the user has any data.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_contexts_for_userid(int $userid) : contextlist {}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The function returns a &#039;&#039;\core_privacy\local\request\contextlist&#039;&#039; which is used to keep a set of contexts together in a fixed fashion.&lt;br /&gt;
&lt;br /&gt;
Because a Subject Access Request covers &#039;&#039;every&#039;&#039; piece of data that is held for a user within Moodle, efficiency and performance is highly important. As a result, contexts are added to the &#039;&#039;contextlist&#039;&#039; by defining one or more SQL queries which return just the contextid. Multiple SQL queries can be added as required. &lt;br /&gt;
&lt;br /&gt;
Many plugins will interact with specific subsystems and store data within them.&lt;br /&gt;
These subsystems will also provide a way in which to link the data that you have stored with your own database tables.&lt;br /&gt;
At present these are still a work in progress and only the &#039;&#039;core_ratings&#039;&#039; subsystem includes this.&lt;br /&gt;
&lt;br /&gt;
======Basic example======&lt;br /&gt;
&lt;br /&gt;
The following example simply fetches the contextid for all forums where a user has a single discussion (note: this is an incomplete example):&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_contexts_for_userid(int $userid) : contextlist {&lt;br /&gt;
        $contextlist = new \core_privacy\local\request\contextlist();&lt;br /&gt;
&lt;br /&gt;
        $sql = &amp;quot;SELECT c.id&lt;br /&gt;
                 FROM {context} c&lt;br /&gt;
           INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel&lt;br /&gt;
           INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname&lt;br /&gt;
           INNER JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
            LEFT JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
                WHERE (&lt;br /&gt;
                d.userid        = :discussionuserid&lt;br /&gt;
                )&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
&lt;br /&gt;
        $params = [&lt;br /&gt;
            &#039;modname&#039;           =&amp;gt; &#039;forum&#039;,&lt;br /&gt;
            &#039;contextlevel&#039;      =&amp;gt; CONTEXT_MODULE,&lt;br /&gt;
            &#039;discussionuserid&#039;  =&amp;gt; $userid,&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        $contextlist-&amp;gt;add_from_sql($sql, $params);&lt;br /&gt;
&lt;br /&gt;
        return $contextlist;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
======More complete example======&lt;br /&gt;
&lt;br /&gt;
The following example includes a link to core_rating. &lt;br /&gt;
It will find any forum, forum discussion, or forum post where the user has any data, including:&lt;br /&gt;
&lt;br /&gt;
* Per-forum digest preferences;&lt;br /&gt;
* Per-forum subscription preferences;&lt;br /&gt;
* Per-forum read tracking preferences;&lt;br /&gt;
* Per-discussion subscription preferences;&lt;br /&gt;
* Per-post read data (if a user has read a post or not); and&lt;br /&gt;
* Per-post rating data.&lt;br /&gt;
&lt;br /&gt;
In the case of the rating data, this will include any post where the user has rated the post of another user.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   int           $userid       The user to search.&lt;br /&gt;
 * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
 */&lt;br /&gt;
public static function get_contexts_for_userid(int $userid) : contextlist {&lt;br /&gt;
    $ratingsql = \core_rating\privacy\provider::get_sql_join(&#039;rat&#039;, &#039;mod_forum&#039;, &#039;post&#039;, &#039;p.id&#039;, $userid);&lt;br /&gt;
    // Fetch all forum discussions, and forum posts.&lt;br /&gt;
    $sql = &amp;quot;SELECT c.id&lt;br /&gt;
                FROM {context} c&lt;br /&gt;
        INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel&lt;br /&gt;
        INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname&lt;br /&gt;
        INNER JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
            LEFT JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
            LEFT JOIN {forum_posts} p ON p.discussion = d.id&lt;br /&gt;
            LEFT JOIN {forum_digests} dig ON dig.forum = f.id&lt;br /&gt;
            LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id&lt;br /&gt;
            LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id&lt;br /&gt;
            LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id&lt;br /&gt;
            LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id&lt;br /&gt;
            {$ratingsql-&amp;gt;join}&lt;br /&gt;
                WHERE (&lt;br /&gt;
                p.userid        = :postuserid OR&lt;br /&gt;
                d.userid        = :discussionuserid OR&lt;br /&gt;
                dig.userid      = :digestuserid OR&lt;br /&gt;
                sub.userid      = :subuserid OR&lt;br /&gt;
                pref.userid     = :prefuserid OR&lt;br /&gt;
                hasread.userid  = :hasreaduserid OR&lt;br /&gt;
                dsub.userid     = :dsubuserid OR&lt;br /&gt;
                {$ratingsql-&amp;gt;userwhere}&lt;br /&gt;
            )&lt;br /&gt;
    &amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    $params = [&lt;br /&gt;
        &#039;modname&#039;           =&amp;gt; &#039;forum&#039;,&lt;br /&gt;
        &#039;contextlevel&#039;      =&amp;gt; CONTEXT_MODULE,&lt;br /&gt;
        &#039;postuserid&#039;        =&amp;gt; $userid,&lt;br /&gt;
        &#039;discussionuserid&#039;  =&amp;gt; $userid,&lt;br /&gt;
        &#039;digestuserid&#039;      =&amp;gt; $userid,&lt;br /&gt;
        &#039;subuserid&#039;         =&amp;gt; $userid,&lt;br /&gt;
        &#039;prefuserid&#039;        =&amp;gt; $userid,&lt;br /&gt;
        &#039;hasreaduserid&#039;     =&amp;gt; $userid,&lt;br /&gt;
        &#039;dsubuserid&#039;        =&amp;gt; $userid,&lt;br /&gt;
    ];&lt;br /&gt;
    $params += $ratingsql-&amp;gt;params;&lt;br /&gt;
&lt;br /&gt;
    $contextlist = new \core_privacy\local\request\contextlist();&lt;br /&gt;
    $contextlist-&amp;gt;add_from_sql($sql, $params);&lt;br /&gt;
&lt;br /&gt;
    return $contextlist;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Retrieving the users in a context=====&lt;br /&gt;
&lt;br /&gt;
You are required to return the list of users holding personal data in a context for which the plugin stores data about the user. This should only be data from that one context. Do not include subcontexts.&lt;br /&gt;
This method is very similar to the &#039;&#039;get_contexts_for_userid&#039;&#039; function but has some important distinctions:&lt;br /&gt;
&lt;br /&gt;
* It takes a &#039;&#039;userlist&#039;&#039; as an argument and does not require that you, as a developer, create it yourself;&lt;br /&gt;
* There is no need to return any value; and&lt;br /&gt;
* The argument for the &#039;&#039;userlist::add_from_sql&#039;&#039; are different to those for &#039;&#039;contextlist::add_from_sql&#039;&#039; (this is because we learnt some important lessons after the initial implementation).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Get the list of users who have data within a context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.&lt;br /&gt;
 */&lt;br /&gt;
public static function get_users_in_context(userlist $userlist) {}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
======Basic example======&lt;br /&gt;
&lt;br /&gt;
The following example simply fetches the userid for all users in a given forum context, who created a discussion or a post in that forum (note: this is an incomplete example):&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Get the list of users who have data within a context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.&lt;br /&gt;
 */&lt;br /&gt;
public static function get_users_in_context(userlist $userlist) {&lt;br /&gt;
&lt;br /&gt;
    $context = $userlist-&amp;gt;get_context();&lt;br /&gt;
&lt;br /&gt;
    if (!$context instanceof \context_module) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $params = [&lt;br /&gt;
        &#039;instanceid&#039;    =&amp;gt; $context-&amp;gt;instanceid,&lt;br /&gt;
        &#039;modulename&#039;    =&amp;gt; &#039;forum&#039;,&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    // Discussion authors.&lt;br /&gt;
    $sql = &amp;quot;SELECT d.userid&lt;br /&gt;
              FROM {course_modules} cm&lt;br /&gt;
              JOIN {modules} m ON m.id = cm.module AND m.name = :modulename&lt;br /&gt;
              JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
              JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
             WHERE cm.id = :instanceid&amp;quot;;&lt;br /&gt;
    $userlist-&amp;gt;add_from_sql(&#039;userid&#039;, $sql, $params);&lt;br /&gt;
&lt;br /&gt;
     // Forum authors.&lt;br /&gt;
    $sql = &amp;quot;SELECT p.userid&lt;br /&gt;
              FROM {course_modules} cm&lt;br /&gt;
              JOIN {modules} m ON m.id = cm.module AND m.name = :modulename&lt;br /&gt;
              JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
              JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
              JOIN {forum_posts} p ON d.id = p.discussion&lt;br /&gt;
             WHERE cm.id = :instanceid&amp;quot;;&lt;br /&gt;
    $userlist-&amp;gt;add_from_sql(&#039;userid&#039;, $sql, $params);&lt;br /&gt;
&lt;br /&gt;
    // ...&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Exporting user data=====&lt;br /&gt;
&lt;br /&gt;
After determining where in Moodle your plugin holds data about a user, the &#039;&#039;\core_privacy\manager&#039;&#039; will then ask your plugin to export all user data for a subset of those locations.&lt;br /&gt;
&lt;br /&gt;
This is achieved through use of the &#039;&#039;export_user_data&#039;&#039; function which takes the list of approved contexts in a &#039;&#039;\core_privacy\local\request\approved_contextlist&#039;&#039; object.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Export all user data for the specified user, in the specified contexts, using the supplied exporter instance.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   approved_contextlist    $contextlist    The approved contexts to export information for.&lt;br /&gt;
 */&lt;br /&gt;
public static function export_user_data(approved_contextlist $contextlist) {}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;approved_contextlist&#039;&#039; includes both the user record, and a list of contexts, which can be retrieved by either processing it as an Iterator, or by calling &#039;&#039;get_contextids()&#039;&#039; or &#039;&#039;get_contexts()&#039;&#039; as required.&lt;br /&gt;
&lt;br /&gt;
Data is exported using a &#039;&#039;\core_privacy\local\request\content_writer&#039;&#039;, which is described in further detail below.&lt;br /&gt;
&lt;br /&gt;
====Plugins which store user preferences====&lt;br /&gt;
&lt;br /&gt;
Many plugins store a variety of user preferences, and must therefore export them.&lt;br /&gt;
&lt;br /&gt;
Since user preferences are a site-wide preference, these are exported separately to other user data.&lt;br /&gt;
In some cases the only data present is user preference data, whilst in others there is a combination of user-provided data, and user preferences.&lt;br /&gt;
&lt;br /&gt;
Storing of user preferences is achieved through implementation of the &#039;&#039;\core_privacy\local\request\user_preference_provider&#039;&#039; interface which defines one required function -- &#039;&#039;export_user_preferences&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
You need to provide a description of the value of the user preference. (This description is particularly useful in cases where the value might be, say, 3, but it actually means &#039;Alphabetical order&#039;.) Most likely you can use an existing language string from your plugin.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Export all user preferences for the plugin.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   int         $userid The userid of the user whose data is to be exported.&lt;br /&gt;
 */&lt;br /&gt;
public static function export_user_preferences(int $userid) {&lt;br /&gt;
    $markasreadonnotification = get_user_preference(&#039;markasreadonnotification&#039;, null, $userid);&lt;br /&gt;
    if (null !== $markasreadonnotification) {&lt;br /&gt;
        switch ($markasreadonnotification) {&lt;br /&gt;
            case 0:&lt;br /&gt;
                $markasreadonnotificationdescription = get_string(&#039;markasreadonnotificationno&#039;, &#039;mod_forum&#039;);&lt;br /&gt;
                break;&lt;br /&gt;
            case 1:&lt;br /&gt;
            default:&lt;br /&gt;
                $markasreadonnotificationdescription = get_string(&#039;markasreadonnotificationyes&#039;, &#039;mod_forum&#039;);&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
        writer::export_user_preference(&#039;mod_forum&#039;, &#039;markasreadonnotification&#039;, $markasreadonnotification, $markasreadonnotificationdescription);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Plugins which can have own subplugins ====&lt;br /&gt;
&lt;br /&gt;
Many plugin types are also able to define their own subplugins and will need to define a contract between themselves and their subplugins in order to fetch their data.&lt;br /&gt;
&lt;br /&gt;
This is required as the parent plugin and the child subplugin should be separate entities and the parent plugin must be able to function if one or more of its subplugins are uninstalled.&lt;br /&gt;
&lt;br /&gt;
The parent plugin is responsible for defining the contract,  and for interacting with its subplugins, though we intend to create helpers to make this easier.&lt;br /&gt;
&lt;br /&gt;
The parent plugin should define a new interface for each type of subplugin that it defines. This interface should extend the &#039;&#039;\core_privacy\local\request\plugin\subplugin_provider&#039;&#039; interface.&lt;br /&gt;
&lt;br /&gt;
===== When a parent plugin should and should not provide the interface for its subplugins =====&lt;br /&gt;
&lt;br /&gt;
There can be cases when there is no point for a plugin to provide the &amp;quot;subplugin_provider&amp;quot; based interface, even if it has own subplugins. See the Atto or TinyMCE editors as real examples.&lt;br /&gt;
&lt;br /&gt;
If the parent plugin has no data passed through to the subplugins, there is no benefit in defining a subplugin provider. For example, Atto subplugins are just used to enhance the functionality and they never receive anything like a context. Most of the time we need to define a subplugin provider, but in cases where there is no data passed from the plugin to its subplugins, there is no need to define the subplugin provider. If the subplugins still do store personal data that are not related to the parent plugin in any way, then subplugins should define their own standard provider.&lt;br /&gt;
&lt;br /&gt;
Compare with something like mod_assign where the subplugins store data for the parent and that data is contextually relevant to the parent plugin. In those cases the subplugin stores data for the plugin and it only makes sense to do so in the context of its parent plugin.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
The following example defines the contract that assign submission subplugins may be required to implement.&lt;br /&gt;
&lt;br /&gt;
The assignment module is responsible for returning the contexts of all assignments where a user has data, but in some cases it is unaware of all of those cases - for example if a Teacher comments on a student submission it may not be aware of these as the information about this interaction may not be stored within its own tables.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/assign/privacy/assignsubmission_provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
namespace mod_assign\privacy;&lt;br /&gt;
use \core_privacy\local\metadata\collection;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
interface assignsubmission_provider extends&lt;br /&gt;
    // This Interface defines a subplugin.&lt;br /&gt;
    \core_privacy\local\request\plugin\subplugin_provider {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the SQL required to find all submission items where this user has had any involvements. &lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @return  \stdClass                   Object containing the join, params, and where used to select a these records from the database.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_items_with_user_interaction(int $userid) : \stdClass ;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Export all relevant user submissions information which match the combination of userid and attemptid.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @param   \context      $context      The context to export this submission against.&lt;br /&gt;
     * @param   array         $subcontext   The subcontext within the context to export this information&lt;br /&gt;
     * @param   int           $attid        The id of the submission to export.&lt;br /&gt;
     */&lt;br /&gt;
    public static function export_user_submissions(int $userid, \context $context, array $subcontext, int $attid) ;&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Plugins which are subplugins to another plugin====&lt;br /&gt;
&lt;br /&gt;
If you are developing a sub-plugin of another plugin, then you will have to look at the relevant plugin in order to determine the exact contract.&lt;br /&gt;
&lt;br /&gt;
Each subplugin type should define a new interface which extends the &#039;&#039;\core_privacy\local\request\plugin\subplugin_provider&#039;&#039; interface and it is up to the parent plugin to define how they will interact with their children.&lt;br /&gt;
&lt;br /&gt;
The principles remain the same, but the exact implementation will differ depending upon requirements.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/pluginname/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
namespace assignsubmission\onlinetext;&lt;br /&gt;
&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin does store personal user data.&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
&lt;br /&gt;
    // This plugin is a subplugin of assign and must meet that contract.&lt;br /&gt;
    \mod_assign\privacy\assignsubmission_provider {&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Plugins which are typically called by a Moodle subsystem====&lt;br /&gt;
&lt;br /&gt;
There are a number of plugintypes in Moodle which are typically called by a specific Moodle subsystem.&lt;br /&gt;
&lt;br /&gt;
Some of these are &#039;&#039;only&#039;&#039; called by that subsystem, for example plugins which are of the &#039;&#039;plagiarism&#039;&#039; plugintype should never be called directly, but are always invoked via the &#039;&#039;core_plagiarism&#039;&#039; subsystem.&lt;br /&gt;
&lt;br /&gt;
Conversely, there maybe other plugintypes which can be called both via a subsystem, and in some other fashion. We are still determining whether any plugintypes currently fit this pattern.&lt;br /&gt;
&lt;br /&gt;
If you are developing a plugin which belongs to a specific subsystem, then you will have to look at the relevant plugin in order to determine the exact contract.&lt;br /&gt;
&lt;br /&gt;
Each subsystem will define a new interface which extends the &#039;&#039;\core_privacy\local\request\plugin\subsystem_provider&#039;&#039; interface and it is up to that subsystem to define how they will interact with those plugins.&lt;br /&gt;
&lt;br /&gt;
The principles remain the same, but the exact implementation will differ depending upon requirements.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;plagiarism/detectorator/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
namespace plagiarism_detectorator\privacy;&lt;br /&gt;
&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin does export personal user data.&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
&lt;br /&gt;
    // This plugin is always linked against another activity module via the Plagiarism API.&lt;br /&gt;
    \core_plagiarism\privacy\plugin_provider {&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Exporting data====&lt;br /&gt;
&lt;br /&gt;
Any plugin which stores data must also export it.&lt;br /&gt;
&lt;br /&gt;
To cater for this the privacy API includes a &#039;&#039;\core_privacy\local\request\content_writer&#039;&#039;, which defines a set of functions to store different types of data.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking data is broken into the following types:&lt;br /&gt;
&lt;br /&gt;
* Data - this is the object being described. For example the post content in a forum post;&lt;br /&gt;
* Related data - this is data related to the object being stored. For example, ratings of a forum post;&lt;br /&gt;
* Metadata - This is metadata about the main object. For example whether you are subscribed to a forum discussion;&lt;br /&gt;
* User preferences - this is data about a site-wide preference;&lt;br /&gt;
* Files - Any files that you are stored within Moodle on behalf of this plugin; and&lt;br /&gt;
* Custom files - For custom file formats - e.g. a calendar feed for calendar data. These should be used sparingly.&lt;br /&gt;
&lt;br /&gt;
Each piece of data is stored against a specific Moodle &#039;&#039;context&#039;&#039;, which will define how the data is structured within the exporter.&lt;br /&gt;
Data, and Related data only accept the &#039;&#039;stdClass&#039;&#039; object, whilst metadata should be stored as a set of key/value pairs which include a description.&lt;br /&gt;
&lt;br /&gt;
In some cases the data being stored belongs within an implicit structure. For example, one forum has many forum discussions, which each have a number of forum posts. This structure is represented by an &#039;&#039;array&#039;&#039; referred to as a &#039;&#039;subcontext&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;content_writer&#039;&#039; must &#039;&#039;always&#039;&#039; be called with a specific context, and can be called as follows:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
use \core_privacy\local\request\writer;&lt;br /&gt;
&lt;br /&gt;
writer::with_context($context)&lt;br /&gt;
    -&amp;gt;export_data($subcontext, $post)&lt;br /&gt;
    -&amp;gt;export_area_files($subcontext, &#039;mod_forum&#039;, &#039;post&#039;, $post-&amp;gt;id)&lt;br /&gt;
    -&amp;gt;export_metadata($subcontext, &#039;postread&#039;, (object) [&#039;firstread&#039; =&amp;gt; $firstread], new \lang_string(&#039;privacy:export:post:postread&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Any text field which supports Moodle files must also be rewritten:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
use \core_privacy\local\request\writer;&lt;br /&gt;
&lt;br /&gt;
$post-&amp;gt;message = writer::with_context($context)&lt;br /&gt;
    -&amp;gt;rewrite_pluginfile_urls($subcontext, &#039;mod_forum&#039;, &#039;post&#039;, $post-&amp;gt;id, $post-&amp;gt;message);&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Providing a way to delete user data===&lt;br /&gt;
&lt;br /&gt;
Deleting user data is also implemented in the request interface. There are two methods that need to be created. The first one to remove all user data from a context, the other to remove user data for a specific user in a list of contexts.&lt;br /&gt;
&lt;br /&gt;
====Delete for a context====&lt;br /&gt;
&lt;br /&gt;
A context is given and all user data (for all users) is to be deleted from the plugin. This will be called when the retention period for the context has expired to adhere to the privacy by design requirement. Retention periods are set in the Data registry.&lt;br /&gt;
&lt;br /&gt;
Note that this will be called for &#039;&#039;&#039;any&#039;&#039;&#039; context being expired, not only those the plugin holds data for (as returned by get_contexts_for_userid()), so you must carefully check the contexts given.&lt;br /&gt;
&lt;br /&gt;
When expiring content for a high-level context such as a course context, the function will be called not only with the course context but also with each context within the course: each module context, each block context, etc. At each level you should only expire data specifically related to that exact context. For a module plugin, this generally means you should only take action for CONTEXT_MODULE contexts and only if they relate to an instance of your module, as shown in the following example. (In the rare case where your module also stores user data related to the course, and not just for a module instance, then you do need to implement CONTEXT_COURSE level contexts to expire that course-level data.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Delete all personal data for all users in the specified context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param context $context Context to delete data from.&lt;br /&gt;
 */&lt;br /&gt;
public static function delete_data_for_all_users_in_context(\context $context) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    if ($context-&amp;gt;contextlevel != CONTEXT_MODULE) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $cm = get_coursemodule_from_id(&#039;choice&#039;, $context-&amp;gt;instanceid);&lt;br /&gt;
    if (!$cm) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $DB-&amp;gt;delete_records(&#039;choice_answers&#039;, [&#039;choiceid&#039; =&amp;gt; $cm-&amp;gt;instance]);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Delete personal information for a specific user and context(s)====&lt;br /&gt;
&lt;br /&gt;
An &#039;&#039;approved_contextlist&#039;&#039; is given and user data related to that user should either be completely deleted, or overwritten if a structure needs to be maintained. This will be called when a user has requested the right to be forgotten. All attempts should be made to delete this data where practical while still allowing the plugin to be used by other users.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/choice/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function delete_data_for_user(approved_contextlist $contextlist) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
    &lt;br /&gt;
    if (empty($contextlist-&amp;gt;count())) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
    $userid = $contextlist-&amp;gt;get_user()-&amp;gt;id;&lt;br /&gt;
    foreach ($contextlist-&amp;gt;get_contexts() as $context) {&lt;br /&gt;
        $instanceid = $DB-&amp;gt;get_field(&#039;course_modules&#039;, &#039;instance&#039;, [&#039;id&#039; =&amp;gt; $context-&amp;gt;instanceid], MUST_EXIST);&lt;br /&gt;
        $DB-&amp;gt;delete_records(&#039;choice_answers&#039;, [&#039;choiceid&#039; =&amp;gt; $instanceid, &#039;userid&#039; =&amp;gt; $userid]);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Delete personal information for several users in a specific context====&lt;br /&gt;
&lt;br /&gt;
An &#039;&#039;approved_userlist&#039;&#039; is given and user data related to all users in the specified context should either be completely deleted, or overwritten if a structure needs to be maintained. This will be called when a user has requested the right to be forgotten when per-role overrides exist, or when performing a per-role expiry of a context. All attempts should be made to delete this data where practical while still allowing the plugin to be used by other users.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/chat/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Delete multiple users within a single context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param approved_userlist $userlist The approved context and user information to delete information for.&lt;br /&gt;
 */&lt;br /&gt;
public static function delete_data_for_users(approved_userlist $userlist) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    $context = $userlist-&amp;gt;get_context();&lt;br /&gt;
    $cm = $DB-&amp;gt;get_record(&#039;course_modules&#039;, [&#039;id&#039; =&amp;gt; $context-&amp;gt;instanceid]);&lt;br /&gt;
    $chat = $DB-&amp;gt;get_record(&#039;chat&#039;, [&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance]);&lt;br /&gt;
&lt;br /&gt;
    list($userinsql, $userinparams) = $DB-&amp;gt;get_in_or_equal($userlist-&amp;gt;get_userids(), SQL_PARAMS_NAMED);&lt;br /&gt;
    $params = array_merge([&#039;chatid&#039; =&amp;gt; $chat-&amp;gt;id], $userinparams);&lt;br /&gt;
    $sql = &amp;quot;chatid = :chatid AND userid {$userinsql}&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    $DB-&amp;gt;delete_records_select(&#039;chat_messages&#039;, $sql, $params);&lt;br /&gt;
    $DB-&amp;gt;delete_records_select(&#039;chat_messages_current&#039;, $sql, $params);&lt;br /&gt;
    $DB-&amp;gt;delete_records_select(&#039;chat_users&#039;, $sql, $params);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Difference between Moodle 3.3 and more recent versions==&lt;br /&gt;
Moodle 3.3 has a minimum requirement of php 5.6 and so type hinting and return type declarations are not supported in this version. &lt;br /&gt;
Consequently the privacy API for this version does not have these features.&lt;br /&gt;
==Common Questions==&lt;br /&gt;
===What to do if you have one plugin that supports multiple branches===&lt;br /&gt;
This is something that we have considered and we have put in place a polyfill. This gets around the restrictions of one version having type hinting and return type declarations while another does not.&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
To use the polyfill include the legacy polyfill trait and create the necessary static methods but with an underscore (shown below).&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
class provider implements&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
    \core_privacy\local\request\plugin\provider {&lt;br /&gt;
&lt;br /&gt;
    // This trait must be included.&lt;br /&gt;
    use \core_privacy\local\legacy_polyfill;&lt;br /&gt;
&lt;br /&gt;
    // The required methods must be in this format starting with an underscore.&lt;br /&gt;
    public static function _get_metadata(collection $collection) {&lt;br /&gt;
        // Code for returning metadata goes here.&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===What to do if your plugin must implement a subplugin or subsystem plugin provider===&lt;br /&gt;
For subplugins (e.g. assignsubmission, assignfeedback, quiz report, quiz access rules), or subsystems which have a plugintype relationship (portfolio, plagiarism, and others), they will also define their own legacy polyfill.&lt;br /&gt;
&lt;br /&gt;
In this instance you will need to include the trait for both the core polyfill, and the provider polyfill as appropriate.&lt;br /&gt;
==== Example ====&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin has data and must therefore define the metadata provider in order to describe it.&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
&lt;br /&gt;
    // This is a plagiarism plugin. It interacts with the plagiarism subsystem rather than with core.&lt;br /&gt;
    \core_plagiarism\privacy\plagiarism_provider {&lt;br /&gt;
&lt;br /&gt;
    // This trait must be included to provide the relevant polyfill for the metadata provider.&lt;br /&gt;
    use \core_privacy\local\legacy_polyfill;&lt;br /&gt;
&lt;br /&gt;
    // This trait must be included to provide the relevant polyfill for the plagirism provider.&lt;br /&gt;
    use \core_plagiarism\privacy\plagiarism_provider\legacy_polyfill;&lt;br /&gt;
&lt;br /&gt;
    // The required methods must be in this format starting with an underscore.&lt;br /&gt;
    public static function _get_metadata(collection $collection) {&lt;br /&gt;
        // Code for returning metadata goes here.&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // This is one of the polyfilled methods from the plagiarism provider.&lt;br /&gt;
    public static function _export_plagiarism_user_data($userid, \context $context, array $subcontext, array $linkarray) {&lt;br /&gt;
        // ...&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Tips for development ==&lt;br /&gt;
&lt;br /&gt;
* While implementing the privacy API into your plugin, there are CLI scripts that can help you to test things on the fly. Just don&#039;t forget these are not supposed to replace proper unit tests. See [[Privacy API/Utilities]] for details.&lt;br /&gt;
* Inherit Unit tests from the &amp;lt;code php&amp;gt;core_privacy\tests\provider_testcase&amp;lt;/code&amp;gt;, not &amp;lt;code php&amp;gt;advanced_testcase&amp;lt;/code&amp;gt;. Advanced test case doesn&#039;t reset the Privacy content_writer between tests!&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Subject Access Request FAQ]]&lt;br /&gt;
* [[:en:GDPR|GDPR]] in the user documentation&lt;br /&gt;
* [[Privacy API/Utilities]] provides CLI scripts that are helpful during development&lt;br /&gt;
&lt;br /&gt;
[[Category:Privacy]]&lt;br /&gt;
[[Category:GDPR]]&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=55936</id>
		<title>Running acceptance test</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=55936"/>
		<updated>2019-04-17T09:46:33Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* 3. Set up Moodle */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Short version ==&lt;br /&gt;
&lt;br /&gt;
...or how I got it to work on Ubuntu and some of the problems encountered. &lt;br /&gt;
&lt;br /&gt;
You need a bunch of browsers and terminal windows open to do this.&lt;br /&gt;
&lt;br /&gt;
==== 1. Background ====  &lt;br /&gt;
&lt;br /&gt;
# I am using the desktop version of Ubuntu 17.04 so there are no issues about running this software in headless mode. Running in headless mode was not tested.&lt;br /&gt;
# Moodle is version 3.3 and is a fully installed and working version using the &#039;standard&#039; Ubuntu LAMP stack.&lt;br /&gt;
&lt;br /&gt;
==== 2. Set up Selenium ====&lt;br /&gt;
&lt;br /&gt;
# Download the Selenium Standalone Server from [http://www.seleniumhq.org/download/ http://www.seleniumhq.org/download/]. It&#039;s a single JAR file, put it anywhere handy.&lt;br /&gt;
# Download the Chrome driver from [https://sites.google.com/a/chromium.org/chromedriver/ https://sites.google.com/a/chromium.org/chromedriver/] (Forget trying to use Firefox, the latest version is not compatible)&lt;br /&gt;
# Unzip the driver (it&#039;s a single file) and copy to /usr/local/bin (should work anywhere on the path)&lt;br /&gt;
# If not installed already, &#039;&amp;lt;tt&amp;gt;sudo apt install default-jre&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Start Selenium - &#039;&amp;lt;tt&amp;gt;java -jar /path/to/your/selenium/server/selenium-server-standalone-N.NN.N.jar -port 4444&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# check it works, access &#039;localhost:4444/wd/hub/&#039; in your browser and check you can create a new Chrome session.&lt;br /&gt;
&lt;br /&gt;
If running headless or the above doesn&#039;t work (&amp;quot;Selenium server is not running&amp;quot; when running the behat tests). Try the following&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;Xvfb -ac :99 -screen 0 1280x1024x16 &amp;amp;&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Then immediately, &#039;&amp;lt;tt&amp;gt;export DISPLAY=:99&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# The run the Selenium command as above&lt;br /&gt;
&lt;br /&gt;
==== 3. Set up Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Create a new &#039;dataroot&#039; area for files especially for behat adjusting permissions accordingly. &lt;br /&gt;
# If not there already, add Section 11 from config-dist.php to your config.php file and review the settings. &lt;br /&gt;
# $CFG-&amp;gt;behat_wwwroot needs to point to your Moodle site yet be different from the &#039;normal&#039; wwwroot (e.g. if you used localhost for wwwroot use 127.0.0.1 for the behat_wwwroot). Whatever you choose, make sure it works. &lt;br /&gt;
# $CFG-&amp;gt;behat_dataroot should point to the directory you created above&lt;br /&gt;
# $CFG-&amp;gt;behat_prefix should be fine. &lt;br /&gt;
# Set up $CFG-&amp;gt;behat_profiles to select Chrome as the browser...&lt;br /&gt;
&lt;br /&gt;
     $CFG-&amp;gt;behat_profiles = [&lt;br /&gt;
         &#039;default&#039; =&amp;gt; [&lt;br /&gt;
             &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
             &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
                 &#039;Behat\MinkExtension&#039; =&amp;gt; [&lt;br /&gt;
                     &#039;selenium2&#039; =&amp;gt; [&lt;br /&gt;
                         &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                     ]&lt;br /&gt;
                 ]&lt;br /&gt;
             ]&lt;br /&gt;
         ]&lt;br /&gt;
     ];&lt;br /&gt;
&lt;br /&gt;
==== 4. Configure Behat for Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;php admin/tool/behat/cli/init.php&amp;lt;/tt&amp;gt;&#039; (from the root of your Moodle install). This installs all the required software and creates the test version of Moodle. &lt;br /&gt;
&lt;br /&gt;
==== 5. Run Behat tests ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;vendor/bin/behat&amp;lt;/tt&amp;gt;&#039;. If you don&#039;t want to run all the tests add &#039;&amp;lt;tt&amp;gt;--tags=&amp;quot;@something&amp;quot;&amp;lt;/tt&amp;gt;&#039; where the @something refers to the tags at the top of most feature files. Use comma-separated list like &#039;&amp;lt;tt&amp;gt;@some_thing,@some_thing_else&amp;lt;/tt&amp;gt;&#039; to run tests from multiple areas. See upstream documentation on Gherkin filters for advanced syntax and more complex examples.&lt;br /&gt;
# After some initial setup dots should start to go by. It&#039;s a while before Selenium is first accessed. On the Linux desktop a new Chrome window appears and the testing process &#039;remote control&#039; starts (hopefully!)&lt;br /&gt;
&lt;br /&gt;
== Prerequisite ==&lt;br /&gt;
Before initializing acceptance test environment for running behat, you should ensure:&lt;br /&gt;
# [[Acceptance_testing#Requirements Meet min. system requirements for running tests]]&lt;br /&gt;
# [[Acceptance_testing#Installation Have set min. config variable in config.php for behat]]&lt;br /&gt;
# [[Acceptance_testing#Installation Downloaded composer dependencies]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Acceptance tests (also known as behat), use [http://www.seleniumhq.org/download/ Selenium server] and can be run as:&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; In single run, only one behat run is executed. So all features are executed in this single run.&lt;br /&gt;
# &#039;&#039;&#039;Parallel runs:&#039;&#039;&#039; (Since Moodle 3.0) Parallel runs allow dev&#039;s to execute multiple behat runs together. This was introduced to get acceptance tests results faster. To achieve this:&lt;br /&gt;
#* Features are divided between multiple behat runs&lt;br /&gt;
#* Symlinks behatrun{x} (x being the run process number), are created pointing to moodle directory, so site for run 1 is accessible via https://localhost/moodle/behatrun1&lt;br /&gt;
#* Process number is included as suffix to $CFG-&amp;gt;behat_prefix.&lt;br /&gt;
#* Process number is suffixed to $CFG-&amp;gt;behat_dataroot.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Initialise acceptance test environment ==&lt;br /&gt;
Before running acceptance tests, environment needs to be initialised for acceptance testing.&lt;br /&gt;
&lt;br /&gt;
=== Single run ===&lt;br /&gt;
For initialising acceptance tests for single run, above command is sufficient.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For initialising acceptance tests for parallel runs, you can use one of the following options&lt;br /&gt;
# &#039;&#039;&#039;-j or --parallel&#039;&#039;&#039; (required) Number of parallel behat run to initialise&lt;br /&gt;
# &#039;&#039;&#039;-m or --maxruns&#039;&#039;&#039;  (optional) Max parallel site which should be initialised at one time. If your system is slow, then you can initialise sites in chucks.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; (optional) Initialise site to run specified run from. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; (optional) Initialise site to run specified run till. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;-o or --optimize-runs&#039;&#039;&#039; (optional) This option will split features with specified tags in all parallel runs, so they are executed first when parallel run gets executed.&lt;br /&gt;
# &#039;&#039;&#039;-a or --add-core-features-to-theme&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified themes (comma separated list of themes)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
// Below command will initialise moodle to run 2 parallel tests.&lt;br /&gt;
php admin/tool/behat/cli/init.php --parallel=2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Running acceptance test environment ==&lt;br /&gt;
=== Single run ===&lt;br /&gt;
Run either of the following commands. For more options &#039;&#039;&#039;vendor/bin/behat --help&#039;&#039;&#039; or http://docs.behat.org/guides/6.cli.html&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You almost always want to limit the number of tests that are run. To run all the tests in one plugin:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml --tags mod_myplugin&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run all the tests in one .feature file:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run a single scenario:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature:40&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here, &#039;40&#039; is the line-number of the feature file where the Scenario starts.&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For running parallel runs, use following command&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/run.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following optional options are available for custom run:&lt;br /&gt;
# &#039;&#039;&#039;--feature&#039;&#039;&#039; Only execute specified feature file (Absolute path of feature file).&lt;br /&gt;
# &#039;&#039;&#039;--suite&#039;&#039;&#039; Features for specified theme will be executed.&lt;br /&gt;
# &#039;&#039;&#039;--replace&#039;&#039;&#039; Replace args string with run process number, useful for output and reruns.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; Execute run starting from (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; Execute run till (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;-a or --add-core-features-to-theme&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified theme&#039;s (comma separated list)&lt;br /&gt;
# Behat options can be passed for filtering features/scenarios:&lt;br /&gt;
#* In case you don&#039;t want to run Javascript tests, use the Behat tags option to skip them, &#039;&#039;&#039;--tags=&amp;quot;~@javascript&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific scenario, use the Behat name option to run it, &#039;&#039;&#039;--name=&amp;quot;Filter user accounts by role and cohort&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific feature file, use the Behat feature option to run it, &#039;&#039;&#039;--feature=&amp;quot;/PATH/TO/MOODLE/admin/tests/behat/filter_users.feature&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Common options for running tests ===&lt;br /&gt;
==== Tests filters ====&lt;br /&gt;
With the &#039;&#039;&#039;--tags&#039;&#039;&#039; or the &#039;&#039;&#039;-name&#039;&#039;&#039; Behat options you can filter which tests are going to run or which ones are going to be skipped. There are a few tags that you might be interested in:&lt;br /&gt;
* &#039;&#039;&#039;@javascript&#039;&#039;&#039;: All the tests that runs in a browser using Javascript; they require Selenium to be running, otherwise an exception will be thrown.&lt;br /&gt;
* &#039;&#039;&#039;@_file_upload&#039;&#039;&#039;: All the tests that involves file uploading or any OS feature that is not 100% part of the browser. They should only be executed when Selenium is running in the same machine where the tests are running.&lt;br /&gt;
* &#039;&#039;&#039;@_alert&#039;&#039;&#039;: All the tests that involves Javascript dialogs (alerts, confirms...) are using a feature that is OS-dependant and out of the browser scope, so they should be tag appropriately as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_window&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; step should be tagged as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_iframe&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; steps should be tagged as it is an advanced feature and some browsers may have problems dealing with them&lt;br /&gt;
* &#039;&#039;&#039;@_cross_browser&#039;&#039;&#039;: All the tests that should run against multiple combinations of browsers + OS in a regular basis. The features that are sensitive to different combinations of OS and browsers should be tagges as @_cross_browser.&lt;br /&gt;
* &#039;&#039;&#039;@componentname&#039;&#039;&#039;: Moodle features uses the [https://docs.moodle.org/dev/Frankenstyle Frankenstyle] component name to tag the features according to the Moodle subsystem they belong to.&lt;br /&gt;
&lt;br /&gt;
==== Output formats ====&lt;br /&gt;
Since Moodle 3.1 option for output is:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=pretty --out=/path/to/pretty.txt --format=moodle_progress --out=std&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Before Moodle 3.1 option for output was:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=&#039;moodle_progress,pretty&#039; --out=&#039;,/path/to/pretty.txt&#039;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following output formats are supported:&lt;br /&gt;
# &#039;&#039;&#039;progress&#039;&#039;&#039;: Prints one character per step.&lt;br /&gt;
# &#039;&#039;&#039;pretty&#039;&#039;&#039;: Prints the feature as is.&lt;br /&gt;
# &#039;&#039;&#039;junit&#039;&#039;&#039;: Outputs the failures in JUnit compatible files.&lt;br /&gt;
# &#039;&#039;&#039;moodle_progress&#039;&#039;&#039;: Prints Moodle branch information and dots for each step.&lt;br /&gt;
# &#039;&#039;&#039;moodle_list&#039;&#039;&#039;: List all scenarios.&lt;br /&gt;
# &#039;&#039;&#039;moodle_stepcount&#039;&#039;&#039;: List all features with total steps in each feature file. Used for parallel run.&lt;br /&gt;
# &#039;&#039;&#039;moodle_screenshot&#039;&#039;&#039;: (since Moodle 3.1) Take screenshot and core dump of each step. With following options you can dump either or both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;image&amp;quot;}&#039;**: will dump image only&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;}&#039;**: will dump html only.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html,image&amp;quot;}&#039;**: will dump both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;, &amp;quot;dir_permissions&amp;quot;: &amp;quot;0777&amp;quot;}&#039;**&lt;br /&gt;
If you want to see the failures immediately (rather than waiting ~3 hours for all the tests to finish) then either use the -v option to output a bit more information, or change the output format using --format. Format &#039;pretty&#039; (&#039;&#039;&#039;-f pretty&#039;&#039;&#039;) is sufficient for most cases, as it outputs each step outcomes in the command line making easier to see the progress.&lt;br /&gt;
&lt;br /&gt;
== Advance usage ==&lt;br /&gt;
=== Rerun failed scenarios ===&lt;br /&gt;
With slow systems or parallel run you might see some random failures, to rerun only failed scenarios (to eliminate random failures), use --rerun option&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; --run=&amp;quot;absolute_path_to_empty_file&amp;quot; (Behat will record failed scenarios in this file, and when run again only failed scenarios will be run)&lt;br /&gt;
# &#039;&#039;&#039;Parallel run:&#039;&#039;&#039; --rerun=&amp;quot;absolute_path_to_empty_file_{runprocess}.txt --replace=&amp;quot;{runprocess}&amp;quot; ({runprocess} will be replaced with the process number for recording fails in the specific run process).&lt;br /&gt;
&#039;&#039;&#039;Since Moodle 3.1 --rerun option don&#039;t accept any value, as it is handled internally by behat&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Running behat with specified theme (Since Moodle 3.2) ===&lt;br /&gt;
You can run behat with any theme installed. To execute behat with specified theme use &#039;&#039;&#039;--suite={THEME_NAME}&#039;&#039;&#039; option, while running behat. By default the features in theme behat folder will be executed for the suite. But if you want to run all core features with the specific theme then initialise acceptance test with -a option. For example, &#039;&#039;&#039;-a {THEME_NAME}&#039;&#039;&#039; e.g.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php -a clean&lt;br /&gt;
vendor/bin/behat --suite=clean --tags=&amp;quot;@enrol_foobar&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That is a core theme but it will work with custom theme. No = or quotes needed around the theme name.&lt;br /&gt;
&lt;br /&gt;
Make sure that &amp;lt;tt&amp;gt;$CFG-&amp;gt;theme&amp;lt;/tt&amp;gt; is &#039;&#039;&#039;not set&#039;&#039;&#039; in your config.php.&lt;br /&gt;
&lt;br /&gt;
==== Override behat core context for theme suite ====&lt;br /&gt;
To override behat step definitions so as to run behat with specified theme, you should create a contexts within &#039;&#039;&#039;/theme/{MYTHEME}/tests/behat/&#039;&#039;&#039; with prefix behat_theme_{MYTHEME}_ and suffixed with the context being overridden. For example, if you want to override behat_mod_forum context, then you should create a class /theme/{MYTHEME}/tests/behat/mod_forum/behat_theme_{MYTHEME}_behat_mod_forum.php&lt;br /&gt;
&lt;br /&gt;
==== Blacklist behat context or features to run in theme suite ====&lt;br /&gt;
To blacklist contexts/ features to be executed by theme suite you should create a /theme/{MYTHEME}/tests/behat/blacklist.json file with following format. Following will not use step_definitions from  behat_grade and behat_navigation while running theme suite. Also, scenarios in auth/tests/behat/login.feature and grade/tests/behat/grade_hidden_items.feature won&#039;t be executed with theme suite.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;contexts&amp;quot;: [&lt;br /&gt;
    &amp;quot;behat_grade&amp;quot;,&lt;br /&gt;
    &amp;quot;behat_navigation&amp;quot;,&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;features&amp;quot;: [&lt;br /&gt;
    &amp;quot;auth/tests/behat/login.feature&amp;quot;,&lt;br /&gt;
    &amp;quot;grade/tests/behat/grade_hidden_items.feature&amp;quot;,&lt;br /&gt;
   ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
==== Override core behat selectors ====&lt;br /&gt;
To override behat selectors in specific theme, you should create a class behat_theme_{MYTHEME}_behat_selectors in /theme/{MYTHEME}/tests/behat/behat_theme_{MYTHEME}_behat_selectors.php extending behat_selectors.&lt;br /&gt;
&lt;br /&gt;
=== Use php built in web server ===&lt;br /&gt;
You can use php built-in-web server for executing behat runs. To do so:&lt;br /&gt;
# Open a command line interface and &#039;&#039;&#039;cd /to/your/moodle/dirroot&#039;&#039;&#039;&lt;br /&gt;
# &#039;&#039;&#039;php -S localhost:8000&#039;&#039;&#039; (This is the test site URL that moodle uses by default, if you want to use another one you can override it in config.php with $CFG-&amp;gt;behat_wwwroot attribute; more info in https://docs.moodle.org/dev/Acceptance_testing#Advanced_usage or config-dist.php)&lt;br /&gt;
# Update $CFG-&amp;gt;behat_wwwroot = localhost:8000; in config.php&lt;br /&gt;
&lt;br /&gt;
=== Define custom options for parallel runs ===&lt;br /&gt;
You can set following custom config options for parallel runs via $CFG-&amp;gt;behat_parallel_run. It&#039;s an array of options where 1st array is for 1st run and so on.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
       array (&lt;br /&gt;
           &#039;dbtype&#039; =&amp;gt; &#039;mysqli&#039;,&lt;br /&gt;
           &#039;dblibrary&#039; =&amp;gt; &#039;native&#039;,&lt;br /&gt;
           &#039;dbhost&#039; =&amp;gt; &#039;localhost&#039;,&lt;br /&gt;
           &#039;dbname&#039; =&amp;gt; &#039;moodletest&#039;,&lt;br /&gt;
           &#039;dbuser&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;dbpass&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;behat_prefix&#039; =&amp;gt; &#039;mdl_&#039;,&lt;br /&gt;
           &#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;,&lt;br /&gt;
           &#039;behat_wwwroot&#039; =&amp;gt; &#039;http://127.0.0.1/moodle&#039;,&lt;br /&gt;
           &#039;behat_dataroot&#039; =&amp;gt; &#039;/home/example/bht_moodledata&#039;&lt;br /&gt;
       )&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To set different selenium servers for parallel runs, you can use following. NOTE: Running parallel (headless) runs on different selenium servers avoid random focus failures.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
    $CFG-&amp;gt;behat_parallel_run = array (&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4445/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4446/wd/hub&#039;),&lt;br /&gt;
    );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Write new tests and behat methods ===&lt;br /&gt;
&lt;br /&gt;
If you want to write tests for your own integration, you can do so by creating new tests with format .feature. Follow instructions in [[Writing_acceptance_tests|this page]] to write new tests. &lt;br /&gt;
&lt;br /&gt;
It is also possible to add new steps the moodle behat integration. In order to do so, you will have to create a new .php class with the prefix &#039;&#039;&#039;behat_&#039;&#039;&#039;. Copy the format from &#039;&#039;&#039;lib\behat\behat_base.php&#039;&#039;&#039;, but set your class to extend the behat_base class instead of the MinkExtension. You can define new behat steps by declaring functions with the appropriate heading. &lt;br /&gt;
&lt;br /&gt;
You will not be  able to use these steps and features right away. Check [[Running_acceptance_test#New_step_definitions_or_features_are_not_executed|this section]] for instructions on how to update the behat integration. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For further information on how to create new steps definitions, check [[Acceptance testing/Custom acceptance steps]].&lt;br /&gt;
&lt;br /&gt;
=== Running acceptance tests with different browser ===&lt;br /&gt;
By default behat will run with Firefox browser through Selenium. By adding the following code to your config.php you can change the selected browser that is run when behat is invoked.  You will need to run php admin/tool/behat/cli/init.php for changes to take effect. Then use --profile=&#039;chrome&#039;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_profiles = array(&lt;br /&gt;
   &#039;chrome&#039; =&amp;gt; array(&lt;br /&gt;
       &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
       &#039;tags&#039; =&amp;gt; &#039;@javascript&#039;,&lt;br /&gt;
   )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
[[Acceptance_testing/Browsers|More info about alternative browsers]]&lt;br /&gt;
&lt;br /&gt;
=== Start multiple selenium servers ===&lt;br /&gt;
From command line Start selenium servers at different ports (say 4444, 4445, 4446 for 3 parallel runs)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4444 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4445 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4446&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternative way of running three Selenium servers in parallel:&lt;br /&gt;
&lt;br /&gt;
 $ printf %d\\n {4444..4446} | xargs -n 1 -P 3 java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port&lt;br /&gt;
&lt;br /&gt;
=== Using Docker to start selenium server ===&lt;br /&gt;
==== What is Docker ====&lt;br /&gt;
Docker is a app container,  it&#039;s a kind of virtual machine, but only for one app, service,  so you can download&lt;br /&gt;
a docker image and run a selenium server without worry in how to configure selenium in your machine, one for chrome, others for firefox, you either don&#039;t need to install the browsers in your machine&lt;br /&gt;
To install docker follow this link; https://docs.docker.com/engine/installation/&lt;br /&gt;
&lt;br /&gt;
==== Selenium docker images ====&lt;br /&gt;
There is many docker images available,  for many browser, the complete list is in https://hub.docker.com/u/selenium/&lt;br /&gt;
for moodle you can use standalone version.&lt;br /&gt;
You can download  specific selenium version too,  for example,  for firefox,  moodle recommend selenium 2.53.1, see: [https://docs.moodle.org/dev/Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium What version do I need?]&lt;br /&gt;
&lt;br /&gt;
so  the command will be:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -d -p4444:4444 selenium/standalone-firefox:2.53.1-beryllium&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
to see all available version click in tags.   For firefox you can find at: https://hub.docker.com/r/selenium/standalone-firefox/tags/&lt;br /&gt;
&lt;br /&gt;
==== Change config.php file ====&lt;br /&gt;
In config.php file you must change the $CFG-&amp;gt;behat_wwwroot=   to your network card (NIC) ip address,  you can&#039;t use &lt;br /&gt;
localhost , 127.0.0.1, ...  or selenium docker server  will fail&lt;br /&gt;
&lt;br /&gt;
=== Increasing timeouts ===&lt;br /&gt;
&lt;br /&gt;
You may see errors such as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
Javascript code and/or AJAX requests are not ready after 10 seconds. &lt;br /&gt;
There is a Javascript error or the code is extremely slow.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Sometimes this indicates a genuine problem with the code, but if you are using a slow computer, it could just mean that the browser was not yet ready. You may find that the test works if you run it again. If you get this error frequently, it might be useful to increase the timeout.&lt;br /&gt;
&lt;br /&gt;
It is possible to increase this timeout by adding a line in your config.php. (Requires Moodle versions 3.5 (from 3.5.6), 3.6 (from 3.6.4), or 3.7+.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_increasetimeout = 2;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will increase all the timeouts by a factor of 2; if that isn&#039;t sufficient, you could use 3. &lt;br /&gt;
&lt;br /&gt;
Increasing timeouts will make tests run a bit slower (because there are points where Behat waits up to a timeout to make sure something doesn&#039;t happen) so don&#039;t do this unless you need to.&lt;br /&gt;
&lt;br /&gt;
== NOTE ==&lt;br /&gt;
# Start the Selenium server (in case you want to run tests that involves Javascript)&lt;br /&gt;
#* (See http://www.installationpage.com/selenium/how-to-run-selenium-headless-firefox-in-ubuntu/ for running &#039;headless&#039; Firefox and xvfm in a server environment)&lt;br /&gt;
#* Open another command line interface and &#039;&#039;&#039;java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Trouble shooting ===&lt;br /&gt;
=== New step definitions or features are not executed === &lt;br /&gt;
If you are adding new tests or steps definitions update the tests list&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --enable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039; For parallel runs, all options for initialising parallel runs are valid &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Tests are failing ===&lt;br /&gt;
If you followed all the steps and you receive an unknown weird error probably your system&#039;s Firefox version is not compatible with the Selenium version you are running.  Please refer Working combinations to ensure you have correct [[Acceptance_testing/Browsers#Working_combinations_of_OS.2BBrowser.2Bselenium]] of them to run acceptance test.&lt;br /&gt;
&lt;br /&gt;
=== Disable acceptance test environment ===&lt;br /&gt;
if you want to prevent access to test environment&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --disable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Note that if you have the HTTP_PROXY environment variable set, which you may have had to do to run composer, then you also need to set NO_PROXY=localhost.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== External links ==&lt;br /&gt;
* Vagrant profile with Moodle and Behat preconfigured: https://github.com/mackensen/moodle-hat&lt;br /&gt;
* Docker containers for Moodle Developers and Behat: https://github.com/moodlehq/moodle-docker&lt;br /&gt;
* Docker environment with Behat preconfigured : https://github.com/tobiga/docker_moodle_environment&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing/Compatibility_changes&amp;diff=55922</id>
		<title>Acceptance testing/Compatibility changes</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing/Compatibility_changes&amp;diff=55922"/>
		<updated>2019-04-15T15:40:43Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* See also */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;As new features are developed for Moodle, some UI changes can cause changes which unfortunately affect behat feature files. This page is intended to document important compatibility changes for behat test developers.&lt;br /&gt;
&lt;br /&gt;
= Compatibility changes =&lt;br /&gt;
== Moodle 3.3 ==&lt;br /&gt;
&lt;br /&gt;
=== And I follow &amp;quot;Course 1&amp;quot; ===&lt;br /&gt;
&amp;lt;b&amp;gt;Summary&amp;lt;/b&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|Previous step/s:&lt;br /&gt;
| And I follow &amp;quot;Course 1&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|New step/s&lt;br /&gt;
|And I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;br&amp;gt;And I am on &amp;quot;Course 1&amp;quot; course homepage with editing mode on&lt;br /&gt;
|-&lt;br /&gt;
|Backported to:&lt;br /&gt;
|Moodle 3.1.6, 3.2.3 and up. &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|+Examples&lt;br /&gt;
|-&lt;br /&gt;
|Before&lt;br /&gt;
|&amp;lt;code&amp;gt;&lt;br /&gt;
Scenario: I am on the course homepage&lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I am on site homepage&lt;br /&gt;
    And I follow &amp;quot;Course 1&amp;quot;&lt;br /&gt;
    Then I should see &amp;quot;Topic 1&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|After&lt;br /&gt;
|&amp;lt;code&amp;gt;&lt;br /&gt;
Scenario: I am on the course homepage&lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I am on &amp;quot;Course 1&amp;quot; course homepage&lt;br /&gt;
    Then I should see &amp;quot;Topic 1&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;b&amp;gt;Why did this change?&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
UI changes in the new dashboard mean the existing &#039;loose&#039; step matches multiple values. The replacement step should help speed up your tests.&lt;br /&gt;
&lt;br /&gt;
== Moodle 3.2 ==&lt;br /&gt;
=== And I click on &amp;quot;Edit settings&amp;quot; &amp;quot;link&amp;quot; in the &amp;quot;Administration&amp;quot; &amp;quot;block&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Summary&amp;lt;/b&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|Previous step/s:&lt;br /&gt;
| And I click on &amp;quot;Edit settings&amp;quot; &amp;quot;link&amp;quot; in the &amp;quot;Administration&amp;quot; &amp;quot;block&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|New step/s&lt;br /&gt;
|And I navigate to &amp;quot;Edit settings&amp;quot; in current page administration&lt;br /&gt;
|-&lt;br /&gt;
|Backported to:&lt;br /&gt;
|Moodle 3.2.1, 3.1.4 and up. &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|+Examples&lt;br /&gt;
|-&lt;br /&gt;
|Before&lt;br /&gt;
|&amp;lt;code&amp;gt;&lt;br /&gt;
Scenario: Going to course settings&lt;br /&gt;
    When I log in as &amp;quot;teacher1&amp;quot;&lt;br /&gt;
    And I follow &amp;quot;Course 1&amp;quot;&lt;br /&gt;
    When I click on &amp;quot;Edit settings&amp;quot; &amp;quot;link&amp;quot; in the &amp;quot;Administration&amp;quot; &amp;quot;block&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|After&lt;br /&gt;
|&amp;lt;code&amp;gt;&lt;br /&gt;
Scenario: Going to course settings&lt;br /&gt;
    When I log in as &amp;quot;teacher1&amp;quot;&lt;br /&gt;
    And I am on &amp;quot;Course 1&amp;quot; course homepage&lt;br /&gt;
    When I navigate to &amp;quot;Edit settings&amp;quot; in current page administration&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;b&amp;gt;Why did this change?&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The boost theme changes how navigation works and the new steps are compatible with boost and bootstrap based themes.&lt;br /&gt;
&lt;br /&gt;
=== And I navigate to &amp;quot;Participants&amp;quot; node in &amp;quot;My courses &amp;gt; C1&amp;quot; ===&lt;br /&gt;
&amp;lt;b&amp;gt; Summary &amp;lt;/b&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|Previous step/s:&lt;br /&gt;
| And I navigate to &amp;quot;Participants&amp;quot; node in &amp;quot;My courses &amp;gt; C1&amp;quot;&amp;lt;br&amp;gt;And I navigate to &amp;quot;Site blogs&amp;quot; node in &amp;quot;Site pages&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|New step/s&lt;br /&gt;
| And I navigate to course participants&lt;br /&gt;
|-&lt;br /&gt;
|Backported to:&lt;br /&gt;
|Moodle 3.2.1, 3.1.4 and up. &lt;br /&gt;
|}&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
|+Examples&lt;br /&gt;
|-&lt;br /&gt;
|Before&lt;br /&gt;
|&amp;lt;code&amp;gt;&lt;br /&gt;
Scenario: Going to course settings&lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I follow &amp;quot;Course 1&amp;quot;&lt;br /&gt;
    And I navigate to &amp;quot;Participants&amp;quot; node in &amp;quot;Current course &amp;gt; C1&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|After&lt;br /&gt;
|&amp;lt;code&amp;gt;&lt;br /&gt;
Scenario: Going to course participants &lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I follow &amp;quot;Course 1&amp;quot;&lt;br /&gt;
    And I navigate to course participants&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;b&amp;gt; Why did this change? &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The boost theme changes how navigation works and the new steps are compatible with boost and bootstrap based themes.&lt;br /&gt;
&lt;br /&gt;
== Moodle 3.1 ==&lt;br /&gt;
=== Behat migration from 2.5 to 3.x ===&lt;br /&gt;
Behat 3 brings a lot of extensibility and modularity but there are compatibility changes for running tests and writing step definitions. See [[Acceptance testing/Migrating from Behat 2.5 to 3.x in Moodle]] for fully details when coming from earlier versions of Moodle.&lt;br /&gt;
&lt;br /&gt;
= See also =&lt;br /&gt;
* [[Running_acceptance_test]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]]&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=55921</id>
		<title>Running acceptance test</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=55921"/>
		<updated>2019-04-15T14:28:29Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Single run */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Short version ==&lt;br /&gt;
&lt;br /&gt;
...or how I got it to work on Ubuntu and some of the problems encountered. &lt;br /&gt;
&lt;br /&gt;
You need a bunch of browsers and terminal windows open to do this.&lt;br /&gt;
&lt;br /&gt;
==== 1. Background ====  &lt;br /&gt;
&lt;br /&gt;
# I am using the desktop version of Ubuntu 17.04 so there are no issues about running this software in headless mode. Running in headless mode was not tested.&lt;br /&gt;
# Moodle is version 3.3 and is a fully installed and working version using the &#039;standard&#039; Ubuntu LAMP stack.&lt;br /&gt;
&lt;br /&gt;
==== 2. Set up Selenium ====&lt;br /&gt;
&lt;br /&gt;
# Download the Selenium Standalone Server from [http://www.seleniumhq.org/download/ http://www.seleniumhq.org/download/]. It&#039;s a single JAR file, put it anywhere handy.&lt;br /&gt;
# Download the Chrome driver from [https://sites.google.com/a/chromium.org/chromedriver/ https://sites.google.com/a/chromium.org/chromedriver/] (Forget trying to use Firefox, the latest version is not compatible)&lt;br /&gt;
# Unzip the driver (it&#039;s a single file) and copy to /usr/local/bin (should work anywhere on the path)&lt;br /&gt;
# If not installed already, &#039;&amp;lt;tt&amp;gt;sudo apt install default-jre&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Start Selenium - &#039;&amp;lt;tt&amp;gt;java -jar /path/to/your/selenium/server/selenium-server-standalone-N.NN.N.jar -port 4444&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# check it works, access &#039;localhost:4444/wd/hub/&#039; in your browser and check you can create a new Chrome session.&lt;br /&gt;
&lt;br /&gt;
If running headless or the above doesn&#039;t work (&amp;quot;Selenium server is not running&amp;quot; when running the behat tests). Try the following&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;Xvfb -ac :99 -screen 0 1280x1024x16 &amp;amp;&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Then immediately, &#039;&amp;lt;tt&amp;gt;export DISPLAY=:99&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# The run the Selenium command as above&lt;br /&gt;
&lt;br /&gt;
==== 3. Set up Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Create a new &#039;dataroot&#039; area for files especially for behat adjusting permissions accordingly. &lt;br /&gt;
# If not there already, add Section 11 from config-dist.php to your config.php file and review the settings. &lt;br /&gt;
# $CFG-&amp;gt;behat_wwwroot needs to point to your Moodle site yet be different from the &#039;normal&#039; wwwroot (e.g. if you used localhost for wwwroot use 127.0.0.1 for the behat_wwwroot). Whatever you choose, make sure it works. &lt;br /&gt;
# $CFG-&amp;gt;behat_dataroot should point to the directory you created above&lt;br /&gt;
# $CFG-&amp;gt;behat_prefix should be fine. &lt;br /&gt;
# Set up $CFG-&amp;gt;behat_profiles to select Chrome as the browser...&lt;br /&gt;
&lt;br /&gt;
    $CFG-&amp;gt;behat_profiles = [&lt;br /&gt;
        &#039;default&#039; =&amp;gt; [&lt;br /&gt;
            &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;Behat\MinkExtension&#039; =&amp;gt; [&lt;br /&gt;
                        &#039;selenium2&#039; =&amp;gt; [&lt;br /&gt;
                            &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                        ]&lt;br /&gt;
                    ]&lt;br /&gt;
                ]&lt;br /&gt;
            ]&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
==== 4. Configure Behat for Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;php admin/tool/behat/cli/init.php&amp;lt;/tt&amp;gt;&#039; (from the root of your Moodle install). This installs all the required software and creates the test version of Moodle. &lt;br /&gt;
&lt;br /&gt;
==== 5. Run Behat tests ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;vendor/bin/behat&amp;lt;/tt&amp;gt;&#039;. If you don&#039;t want to run all the tests add &#039;&amp;lt;tt&amp;gt;--tags=&amp;quot;@something&amp;quot;&amp;lt;/tt&amp;gt;&#039; where the @something refers to the tags at the top of most feature files. Use comma-separated list like &#039;&amp;lt;tt&amp;gt;@some_thing,@some_thing_else&amp;lt;/tt&amp;gt;&#039; to run tests from multiple areas. See upstream documentation on Gherkin filters for advanced syntax and more complex examples.&lt;br /&gt;
# After some initial setup dots should start to go by. It&#039;s a while before Selenium is first accessed. On the Linux desktop a new Chrome window appears and the testing process &#039;remote control&#039; starts (hopefully!)&lt;br /&gt;
&lt;br /&gt;
== Prerequisite ==&lt;br /&gt;
Before initializing acceptance test environment for running behat, you should ensure:&lt;br /&gt;
# [[Acceptance_testing#Requirements Meet min. system requirements for running tests]]&lt;br /&gt;
# [[Acceptance_testing#Installation Have set min. config variable in config.php for behat]]&lt;br /&gt;
# [[Acceptance_testing#Installation Downloaded composer dependencies]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Acceptance tests (also known as behat), use [http://www.seleniumhq.org/download/ Selenium server] and can be run as:&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; In single run, only one behat run is executed. So all features are executed in this single run.&lt;br /&gt;
# &#039;&#039;&#039;Parallel runs:&#039;&#039;&#039; (Since Moodle 3.0) Parallel runs allow dev&#039;s to execute multiple behat runs together. This was introduced to get acceptance tests results faster. To achieve this:&lt;br /&gt;
#* Features are divided between multiple behat runs&lt;br /&gt;
#* Symlinks behatrun{x} (x being the run process number), are created pointing to moodle directory, so site for run 1 is accessible via https://localhost/moodle/behatrun1&lt;br /&gt;
#* Process number is included as suffix to $CFG-&amp;gt;behat_prefix.&lt;br /&gt;
#* Process number is suffixed to $CFG-&amp;gt;behat_dataroot.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Initialise acceptance test environment ==&lt;br /&gt;
Before running acceptance tests, environment needs to be initialised for acceptance testing.&lt;br /&gt;
&lt;br /&gt;
=== Single run ===&lt;br /&gt;
For initialising acceptance tests for single run, above command is sufficient.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For initialising acceptance tests for parallel runs, you can use one of the following options&lt;br /&gt;
# &#039;&#039;&#039;-j or --parallel&#039;&#039;&#039; (required) Number of parallel behat run to initialise&lt;br /&gt;
# &#039;&#039;&#039;-m or --maxruns&#039;&#039;&#039;  (optional) Max parallel site which should be initialised at one time. If your system is slow, then you can initialise sites in chucks.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; (optional) Initialise site to run specified run from. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; (optional) Initialise site to run specified run till. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;-o or --optimize-runs&#039;&#039;&#039; (optional) This option will split features with specified tags in all parallel runs, so they are executed first when parallel run gets executed.&lt;br /&gt;
# &#039;&#039;&#039;-a or --add-core-features-to-theme&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified themes (comma separated list of themes)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
// Below command will initialise moodle to run 2 parallel tests.&lt;br /&gt;
php admin/tool/behat/cli/init.php --parallel=2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Running acceptance test environment ==&lt;br /&gt;
=== Single run ===&lt;br /&gt;
Run either of the following commands. For more options &#039;&#039;&#039;vendor/bin/behat --help&#039;&#039;&#039; or http://docs.behat.org/guides/6.cli.html&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You almost always want to limit the number of tests that are run. To run all the tests in one plugin:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml --tags mod_myplugin&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run all the tests in one .feature file:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run a single scenario:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature:40&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here, &#039;40&#039; is the line-number of the feature file where the Scenario starts.&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For running parallel runs, use following command&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/run.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following optional options are available for custom run:&lt;br /&gt;
# &#039;&#039;&#039;--feature&#039;&#039;&#039; Only execute specified feature file (Absolute path of feature file).&lt;br /&gt;
# &#039;&#039;&#039;--suite&#039;&#039;&#039; Features for specified theme will be executed.&lt;br /&gt;
# &#039;&#039;&#039;--replace&#039;&#039;&#039; Replace args string with run process number, useful for output and reruns.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; Execute run starting from (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; Execute run till (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;-a or --add-core-features-to-theme&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified theme&#039;s (comma separated list)&lt;br /&gt;
# Behat options can be passed for filtering features/scenarios:&lt;br /&gt;
#* In case you don&#039;t want to run Javascript tests, use the Behat tags option to skip them, &#039;&#039;&#039;--tags=&amp;quot;~@javascript&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific scenario, use the Behat name option to run it, &#039;&#039;&#039;--name=&amp;quot;Filter user accounts by role and cohort&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific feature file, use the Behat feature option to run it, &#039;&#039;&#039;--feature=&amp;quot;/PATH/TO/MOODLE/admin/tests/behat/filter_users.feature&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Common options for running tests ===&lt;br /&gt;
==== Tests filters ====&lt;br /&gt;
With the &#039;&#039;&#039;--tags&#039;&#039;&#039; or the &#039;&#039;&#039;-name&#039;&#039;&#039; Behat options you can filter which tests are going to run or which ones are going to be skipped. There are a few tags that you might be interested in:&lt;br /&gt;
* &#039;&#039;&#039;@javascript&#039;&#039;&#039;: All the tests that runs in a browser using Javascript; they require Selenium to be running, otherwise an exception will be thrown.&lt;br /&gt;
* &#039;&#039;&#039;@_file_upload&#039;&#039;&#039;: All the tests that involves file uploading or any OS feature that is not 100% part of the browser. They should only be executed when Selenium is running in the same machine where the tests are running.&lt;br /&gt;
* &#039;&#039;&#039;@_alert&#039;&#039;&#039;: All the tests that involves Javascript dialogs (alerts, confirms...) are using a feature that is OS-dependant and out of the browser scope, so they should be tag appropriately as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_window&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; step should be tagged as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_iframe&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; steps should be tagged as it is an advanced feature and some browsers may have problems dealing with them&lt;br /&gt;
* &#039;&#039;&#039;@_cross_browser&#039;&#039;&#039;: All the tests that should run against multiple combinations of browsers + OS in a regular basis. The features that are sensitive to different combinations of OS and browsers should be tagges as @_cross_browser.&lt;br /&gt;
* &#039;&#039;&#039;@componentname&#039;&#039;&#039;: Moodle features uses the [https://docs.moodle.org/dev/Frankenstyle Frankenstyle] component name to tag the features according to the Moodle subsystem they belong to.&lt;br /&gt;
&lt;br /&gt;
==== Output formats ====&lt;br /&gt;
Since Moodle 3.1 option for output is:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=pretty --out=/path/to/pretty.txt --format=moodle_progress --out=std&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Before Moodle 3.1 option for output was:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=&#039;moodle_progress,pretty&#039; --out=&#039;,/path/to/pretty.txt&#039;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following output formats are supported:&lt;br /&gt;
# &#039;&#039;&#039;progress&#039;&#039;&#039;: Prints one character per step.&lt;br /&gt;
# &#039;&#039;&#039;pretty&#039;&#039;&#039;: Prints the feature as is.&lt;br /&gt;
# &#039;&#039;&#039;junit&#039;&#039;&#039;: Outputs the failures in JUnit compatible files.&lt;br /&gt;
# &#039;&#039;&#039;moodle_progress&#039;&#039;&#039;: Prints Moodle branch information and dots for each step.&lt;br /&gt;
# &#039;&#039;&#039;moodle_list&#039;&#039;&#039;: List all scenarios.&lt;br /&gt;
# &#039;&#039;&#039;moodle_stepcount&#039;&#039;&#039;: List all features with total steps in each feature file. Used for parallel run.&lt;br /&gt;
# &#039;&#039;&#039;moodle_screenshot&#039;&#039;&#039;: (since Moodle 3.1) Take screenshot and core dump of each step. With following options you can dump either or both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;image&amp;quot;}&#039;**: will dump image only&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;}&#039;**: will dump html only.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html,image&amp;quot;}&#039;**: will dump both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;, &amp;quot;dir_permissions&amp;quot;: &amp;quot;0777&amp;quot;}&#039;**&lt;br /&gt;
If you want to see the failures immediately (rather than waiting ~3 hours for all the tests to finish) then either use the -v option to output a bit more information, or change the output format using --format. Format &#039;pretty&#039; (&#039;&#039;&#039;-f pretty&#039;&#039;&#039;) is sufficient for most cases, as it outputs each step outcomes in the command line making easier to see the progress.&lt;br /&gt;
&lt;br /&gt;
== Advance usage ==&lt;br /&gt;
=== Rerun failed scenarios ===&lt;br /&gt;
With slow systems or parallel run you might see some random failures, to rerun only failed scenarios (to eliminate random failures), use --rerun option&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; --run=&amp;quot;absolute_path_to_empty_file&amp;quot; (Behat will record failed scenarios in this file, and when run again only failed scenarios will be run)&lt;br /&gt;
# &#039;&#039;&#039;Parallel run:&#039;&#039;&#039; --rerun=&amp;quot;absolute_path_to_empty_file_{runprocess}.txt --replace=&amp;quot;{runprocess}&amp;quot; ({runprocess} will be replaced with the process number for recording fails in the specific run process).&lt;br /&gt;
&#039;&#039;&#039;Since Moodle 3.1 --rerun option don&#039;t accept any value, as it is handled internally by behat&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Running behat with specified theme (Since Moodle 3.2) ===&lt;br /&gt;
You can run behat with any theme installed. To execute behat with specified theme use &#039;&#039;&#039;--suite={THEME_NAME}&#039;&#039;&#039; option, while running behat. By default the features in theme behat folder will be executed for the suite. But if you want to run all core features with the specific theme then initialise acceptance test with -a option. For example, &#039;&#039;&#039;-a {THEME_NAME}&#039;&#039;&#039; e.g.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php -a clean&lt;br /&gt;
vendor/bin/behat --suite=clean --tags=&amp;quot;@enrol_foobar&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That is a core theme but it will work with custom theme. No = or quotes needed around the theme name.&lt;br /&gt;
&lt;br /&gt;
Make sure that &amp;lt;tt&amp;gt;$CFG-&amp;gt;theme&amp;lt;/tt&amp;gt; is &#039;&#039;&#039;not set&#039;&#039;&#039; in your config.php.&lt;br /&gt;
&lt;br /&gt;
==== Override behat core context for theme suite ====&lt;br /&gt;
To override behat step definitions so as to run behat with specified theme, you should create a contexts within &#039;&#039;&#039;/theme/{MYTHEME}/tests/behat/&#039;&#039;&#039; with prefix behat_theme_{MYTHEME}_ and suffixed with the context being overridden. For example, if you want to override behat_mod_forum context, then you should create a class /theme/{MYTHEME}/tests/behat/mod_forum/behat_theme_{MYTHEME}_behat_mod_forum.php&lt;br /&gt;
&lt;br /&gt;
==== Blacklist behat context or features to run in theme suite ====&lt;br /&gt;
To blacklist contexts/ features to be executed by theme suite you should create a /theme/{MYTHEME}/tests/behat/blacklist.json file with following format. Following will not use step_definitions from  behat_grade and behat_navigation while running theme suite. Also, scenarios in auth/tests/behat/login.feature and grade/tests/behat/grade_hidden_items.feature won&#039;t be executed with theme suite.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;contexts&amp;quot;: [&lt;br /&gt;
    &amp;quot;behat_grade&amp;quot;,&lt;br /&gt;
    &amp;quot;behat_navigation&amp;quot;,&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;features&amp;quot;: [&lt;br /&gt;
    &amp;quot;auth/tests/behat/login.feature&amp;quot;,&lt;br /&gt;
    &amp;quot;grade/tests/behat/grade_hidden_items.feature&amp;quot;,&lt;br /&gt;
   ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
==== Override core behat selectors ====&lt;br /&gt;
To override behat selectors in specific theme, you should create a class behat_theme_{MYTHEME}_behat_selectors in /theme/{MYTHEME}/tests/behat/behat_theme_{MYTHEME}_behat_selectors.php extending behat_selectors.&lt;br /&gt;
&lt;br /&gt;
=== Use php built in web server ===&lt;br /&gt;
You can use php built-in-web server for executing behat runs. To do so:&lt;br /&gt;
# Open a command line interface and &#039;&#039;&#039;cd /to/your/moodle/dirroot&#039;&#039;&#039;&lt;br /&gt;
# &#039;&#039;&#039;php -S localhost:8000&#039;&#039;&#039; (This is the test site URL that moodle uses by default, if you want to use another one you can override it in config.php with $CFG-&amp;gt;behat_wwwroot attribute; more info in https://docs.moodle.org/dev/Acceptance_testing#Advanced_usage or config-dist.php)&lt;br /&gt;
# Update $CFG-&amp;gt;behat_wwwroot = localhost:8000; in config.php&lt;br /&gt;
&lt;br /&gt;
=== Define custom options for parallel runs ===&lt;br /&gt;
You can set following custom config options for parallel runs via $CFG-&amp;gt;behat_parallel_run. It&#039;s an array of options where 1st array is for 1st run and so on.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
       array (&lt;br /&gt;
           &#039;dbtype&#039; =&amp;gt; &#039;mysqli&#039;,&lt;br /&gt;
           &#039;dblibrary&#039; =&amp;gt; &#039;native&#039;,&lt;br /&gt;
           &#039;dbhost&#039; =&amp;gt; &#039;localhost&#039;,&lt;br /&gt;
           &#039;dbname&#039; =&amp;gt; &#039;moodletest&#039;,&lt;br /&gt;
           &#039;dbuser&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;dbpass&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;behat_prefix&#039; =&amp;gt; &#039;mdl_&#039;,&lt;br /&gt;
           &#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;,&lt;br /&gt;
           &#039;behat_wwwroot&#039; =&amp;gt; &#039;http://127.0.0.1/moodle&#039;,&lt;br /&gt;
           &#039;behat_dataroot&#039; =&amp;gt; &#039;/home/example/bht_moodledata&#039;&lt;br /&gt;
       )&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To set different selenium servers for parallel runs, you can use following. NOTE: Running parallel (headless) runs on different selenium servers avoid random focus failures.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
    $CFG-&amp;gt;behat_parallel_run = array (&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4445/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4446/wd/hub&#039;),&lt;br /&gt;
    );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Write new tests and behat methods ===&lt;br /&gt;
&lt;br /&gt;
If you want to write tests for your own integration, you can do so by creating new tests with format .feature. Follow instructions in [[Writing_acceptance_tests|this page]] to write new tests. &lt;br /&gt;
&lt;br /&gt;
It is also possible to add new steps the moodle behat integration. In order to do so, you will have to create a new .php class with the prefix &#039;&#039;&#039;behat_&#039;&#039;&#039;. Copy the format from &#039;&#039;&#039;lib\behat\behat_base.php&#039;&#039;&#039;, but set your class to extend the behat_base class instead of the MinkExtension. You can define new behat steps by declaring functions with the appropriate heading. &lt;br /&gt;
&lt;br /&gt;
You will not be  able to use these steps and features right away. Check [[Running_acceptance_test#New_step_definitions_or_features_are_not_executed|this section]] for instructions on how to update the behat integration. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For further information on how to create new steps definitions, check [[Acceptance testing/Custom acceptance steps]].&lt;br /&gt;
&lt;br /&gt;
=== Running acceptance tests with different browser ===&lt;br /&gt;
By default behat will run with Firefox browser through Selenium. By adding the following code to your config.php you can change the selected browser that is run when behat is invoked.  You will need to run php admin/tool/behat/cli/init.php for changes to take effect. Then use --profile=&#039;chrome&#039;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_profiles = array(&lt;br /&gt;
   &#039;chrome&#039; =&amp;gt; array(&lt;br /&gt;
       &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
       &#039;tags&#039; =&amp;gt; &#039;@javascript&#039;,&lt;br /&gt;
   )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
[[Acceptance_testing/Browsers|More info about alternative browsers]]&lt;br /&gt;
&lt;br /&gt;
=== Start multiple selenium servers ===&lt;br /&gt;
From command line Start selenium servers at different ports (say 4444, 4445, 4446 for 3 parallel runs)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4444 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4445 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4446&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternative way of running three Selenium servers in parallel:&lt;br /&gt;
&lt;br /&gt;
 $ printf %d\\n {4444..4446} | xargs -n 1 -P 3 java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port&lt;br /&gt;
&lt;br /&gt;
=== Using Docker to start selenium server ===&lt;br /&gt;
==== What is Docker ====&lt;br /&gt;
Docker is a app container,  it&#039;s a kind of virtual machine, but only for one app, service,  so you can download&lt;br /&gt;
a docker image and run a selenium server without worry in how to configure selenium in your machine, one for chrome, others for firefox, you either don&#039;t need to install the browsers in your machine&lt;br /&gt;
To install docker follow this link; https://docs.docker.com/engine/installation/&lt;br /&gt;
&lt;br /&gt;
==== Selenium docker images ====&lt;br /&gt;
There is many docker images available,  for many browser, the complete list is in https://hub.docker.com/u/selenium/&lt;br /&gt;
for moodle you can use standalone version.&lt;br /&gt;
You can download  specific selenium version too,  for example,  for firefox,  moodle recommend selenium 2.53.1, see: [https://docs.moodle.org/dev/Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium What version do I need?]&lt;br /&gt;
&lt;br /&gt;
so  the command will be:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -d -p4444:4444 selenium/standalone-firefox:2.53.1-beryllium&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
to see all available version click in tags.   For firefox you can find at: https://hub.docker.com/r/selenium/standalone-firefox/tags/&lt;br /&gt;
&lt;br /&gt;
==== Change config.php file ====&lt;br /&gt;
In config.php file you must change the $CFG-&amp;gt;behat_wwwroot=   to your network card (NIC) ip address,  you can&#039;t use &lt;br /&gt;
localhost , 127.0.0.1, ...  or selenium docker server  will fail&lt;br /&gt;
&lt;br /&gt;
=== Increasing timeouts ===&lt;br /&gt;
&lt;br /&gt;
You may see errors such as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
Javascript code and/or AJAX requests are not ready after 10 seconds. &lt;br /&gt;
There is a Javascript error or the code is extremely slow.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Sometimes this indicates a genuine problem with the code, but if you are using a slow computer, it could just mean that the browser was not yet ready. You may find that the test works if you run it again. If you get this error frequently, it might be useful to increase the timeout.&lt;br /&gt;
&lt;br /&gt;
It is possible to increase this timeout by adding a line in your config.php. (Requires Moodle versions 3.5 (from 3.5.6), 3.6 (from 3.6.4), or 3.7+.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_increasetimeout = 2;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will increase all the timeouts by a factor of 2; if that isn&#039;t sufficient, you could use 3. &lt;br /&gt;
&lt;br /&gt;
Increasing timeouts will make tests run a bit slower (because there are points where Behat waits up to a timeout to make sure something doesn&#039;t happen) so don&#039;t do this unless you need to.&lt;br /&gt;
&lt;br /&gt;
== NOTE ==&lt;br /&gt;
# Start the Selenium server (in case you want to run tests that involves Javascript)&lt;br /&gt;
#* (See http://www.installationpage.com/selenium/how-to-run-selenium-headless-firefox-in-ubuntu/ for running &#039;headless&#039; Firefox and xvfm in a server environment)&lt;br /&gt;
#* Open another command line interface and &#039;&#039;&#039;java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Trouble shooting ===&lt;br /&gt;
=== New step definitions or features are not executed === &lt;br /&gt;
If you are adding new tests or steps definitions update the tests list&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --enable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039; For parallel runs, all options for initialising parallel runs are valid &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Tests are failing ===&lt;br /&gt;
If you followed all the steps and you receive an unknown weird error probably your system&#039;s Firefox version is not compatible with the Selenium version you are running.  Please refer Working combinations to ensure you have correct [[Acceptance_testing/Browsers#Working_combinations_of_OS.2BBrowser.2Bselenium]] of them to run acceptance test.&lt;br /&gt;
&lt;br /&gt;
=== Disable acceptance test environment ===&lt;br /&gt;
if you want to prevent access to test environment&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --disable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Note that if you have the HTTP_PROXY environment variable set, which you may have had to do to run composer, then you also need to set NO_PROXY=localhost.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== External links ==&lt;br /&gt;
* Vagrant profile with Moodle and Behat preconfigured: https://github.com/mackensen/moodle-hat&lt;br /&gt;
* Docker containers for Moodle Developers and Behat: https://github.com/moodlehq/moodle-docker&lt;br /&gt;
* Docker environment with Behat preconfigured : https://github.com/tobiga/docker_moodle_environment&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_acceptance_tests&amp;diff=55920</id>
		<title>Writing acceptance tests</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_acceptance_tests&amp;diff=55920"/>
		<updated>2019-04-15T14:23:34Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
This documentation gives some hints, how to write behat tests for core and for plugins. The focus of the documentation is on behat tests for plugins. They are written in some kind of natural language and describe how the front-end of moodle should behave, when a user interacts with it.&lt;br /&gt;
&lt;br /&gt;
Each test consists of a set of so called steps in a GIVEN, WHEN, THEN style:&lt;br /&gt;
* &#039;&#039;&#039;GIVEN&#039;&#039;&#039;: These steps outline the state of your moodle platform at the start of your test. Here you can create users, courses and plugin instances.&lt;br /&gt;
* &#039;&#039;&#039;WHEN&#039;&#039;&#039;: These steps usually execute the functionality of your plugin, which is under test.&lt;br /&gt;
* &#039;&#039;&#039;THEN&#039;&#039;&#039;: These steps describe the expected behaviour of your plugin. They usually check if different elements can or can&#039;t be seen.&lt;br /&gt;
This matches the standard [http://xunitpatterns.com/Four%20Phase%20Test.html Four-phase test pattern]. The fourth phase is &#039;tear-down&#039; which we don&#039;t need because Behat automatically cleans up after each test.&lt;br /&gt;
&lt;br /&gt;
To initialize and run your tests, please follow the instructions of [[Running_acceptance_test]].&lt;br /&gt;
&lt;br /&gt;
== Create you own tests ==&lt;br /&gt;
Behat tests are located within the directory tests/behat of your plugin.&lt;br /&gt;
The different tests are defined in files with the ending *.feature.&lt;br /&gt;
First, you have to define the header of your test:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@mod @mod_yourplugin @javascript&lt;br /&gt;
Feature: Here comes a description of your user story.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The tags on top of the feature description can be used to select specific test cases when running the tests.&lt;br /&gt;
The &#039;@javascript&#039; tag should only be used, if javascript is needed to execute your test. This is dependent on the step you will use in your definition.&lt;br /&gt;
Javascript tests are usually much slower than tests executed without javascript.&lt;br /&gt;
&lt;br /&gt;
Afterwards you can specify a scenario:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@javascript&lt;br /&gt;
  Scenario: Description of your scenario, which you want to test.&lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I am on &amp;quot;Course 1&amp;quot; course homepage&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Again you can define specific tags. Afterwards you write the steps, which should be executed during your test.&lt;br /&gt;
&lt;br /&gt;
==== Multiple Scenarios ====&lt;br /&gt;
You can have an arbitrary amount of scenarios within a test. Please make sure they all belong to the same feature.&lt;br /&gt;
If you have certain steps, which should be executed for every scenario of a feature, you can define them using a background:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname | category | groupmode |&lt;br /&gt;
      | Course 1 | C1        | 0        | 1         |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username | firstname | lastname | email |&lt;br /&gt;
      | teacher1 | Theo | Teacher | teacher1@example.com |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
This is usually used, to define the different &#039;&#039;&#039;GIVEN&#039;&#039;&#039; steps.&lt;br /&gt;
&lt;br /&gt;
==== Use existing steps ====&lt;br /&gt;
There are different ways how to effectively browse the available existing steps:&lt;br /&gt;
&lt;br /&gt;
====== Moodle Administration ======&lt;br /&gt;
Moodle offers within its administration menu under Site Administration &amp;gt; Development &amp;gt; Acceptance Testing a complete and searchable list of all available step definitions.&lt;br /&gt;
However, make sure you installed the behat test site first!&lt;br /&gt;
&lt;br /&gt;
====== PhpStorm ======&lt;br /&gt;
In PhpStorm or IntelliJ you can install the behat extension. Then you get auto completions within feature files, which helps a lot during behat test development.&lt;br /&gt;
&lt;br /&gt;
==== Providing values to steps ====&lt;br /&gt;
Most of the steps requires values, there are methods to provide values to steps, the method depends on the step specification.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;A string/text&#039;&#039;&#039;; is the most common case, the texts are wrapped between double quotes (&amp;quot; character) you have to replace the info about the expected value for your value; for example something like &#039;&#039;&#039;I press &amp;quot;BUTTON_STRING&amp;quot;&#039;&#039;&#039; should become &#039;&#039;&#039;I press &amp;quot;Save and return to course&amp;quot;&#039;&#039;&#039;. If you want to add a string which contains a &amp;quot; character, you can escape it with \&amp;quot;, for example &#039;&#039;&#039;I fill the &amp;quot;Name&amp;quot; field with &amp;quot;Alan alias \&amp;quot;the legend\&amp;quot;&amp;quot;&#039;&#039;&#039;. You can identify this steps because they ends with &#039;&#039;&#039;_STRING&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;A number&#039;&#039;&#039;; some steps requires numbers as values, to be more specific an undetermined number of digits from 0 to 9 (Natural numbers + 0) you can identify them because the expected value info string ends with &#039;&#039;&#039;_NUMBER&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;A table&#039;&#039;&#039;; is a relation between values, the most common use of it is to fill forms. The steps which requires tables are easily identifiable because they finish with &#039;&#039;&#039;:&#039;&#039;&#039; The steps description gives info about what the table columns must contain, for example &#039;&#039;&#039;Fills a moodle form with field/value data&#039;&#039;&#039;. Here you don&#039;t need to escape the double quotes if you want to include them as part of the value.&lt;br /&gt;
* &#039;&#039;&#039;A PyString&#039;&#039;&#039;; is a multiline string, most commonly used to fill out forms when a newline is required. Like steps with tables, steps which require PyStrings will end with &amp;quot;:&amp;quot;&lt;br /&gt;
* &#039;&#039;&#039;A field value&#039;&#039;&#039;; There are many different field types, if an argument requires a field value the expected value will depend on the field type:&lt;br /&gt;
** Text-based fields: It expects the text. This includes textareas, input type text, input type password...&lt;br /&gt;
** Checkbox: It expects 1 to check and for checked and &amp;quot;&amp;quot; to uncheck or for unchecked&lt;br /&gt;
** Select: It expects the option text or the option value. In case you interact with a multi-select you should specify the options separating them with commas. For example: &#039;&#039;&#039;option1, option2, option3&#039;&#039;&#039;&lt;br /&gt;
** Radio: The text of the radio option&lt;br /&gt;
* &#039;&#039;&#039;A selector&#039;&#039;&#039;; there are steps that can be used with different kinds of elements, for example &#039;&#039;&#039;I click on &amp;quot;User Name&amp;quot; &amp;quot;link&amp;quot;&#039;&#039;&#039; or &#039;&#039;&#039;I click on &amp;quot;User Name&amp;quot; &amp;quot;button&amp;quot;&#039;&#039;&#039; this is a closed list of elements, they always works together with another argument, where you specify the locator (eg. the link text in a link) In the &#039;Acceptance testing&#039; interface you can see a drop-down menu to select one of these options:&lt;br /&gt;
** field - for searching a field by its id, name, value or label&lt;br /&gt;
** link - for searching a link by its href, id, title, img alt or value&lt;br /&gt;
** button - for searching a button by its name, id, value, img alt or title&lt;br /&gt;
** link_or_button - for searching for both, links and buttons&lt;br /&gt;
** select - for searching a select field by its id, name or label&lt;br /&gt;
** checkbox - for searching a checkbox by its id, name, or label&lt;br /&gt;
** radio - for searching a radio button by its id, name, or label&lt;br /&gt;
** file - for searching a file input by its id, name, or label&lt;br /&gt;
** optgroup - for searching optgroup by its label&lt;br /&gt;
** option - for searching an option by its content&lt;br /&gt;
** dialogue - for searching a dialogue with the specified header text&lt;br /&gt;
** filemanager - for searching a filemanager by it&#039;s id or label&lt;br /&gt;
** block - for searching a Moodle block by it&#039;s English name or it&#039;s frankenstyle name&lt;br /&gt;
** section - for searching for a section on a course page by it&#039;s title or its written out date (e.g. &amp;quot;1 January - 7 January&amp;quot;). Use &amp;quot;frontpage&amp;quot; &amp;quot;section&amp;quot; for the frontpage section if it has no title (default)&lt;br /&gt;
** activity - for searching for an activity module in a course list by it&#039;s title&lt;br /&gt;
** region - for searching a Moodle page region with that id, in fact it works with all the page&#039;s ids&lt;br /&gt;
** table_row - for searching a table row which contains the specified text&lt;br /&gt;
** table - for searching a table by its id or caption&lt;br /&gt;
** fieldset - for searching a fieldset by it&#039;s id or legend&lt;br /&gt;
** css_element - for searching an element by its CSS selector&lt;br /&gt;
** xpath_element - for searching an element by its XPath&lt;br /&gt;
* &#039;&#039;&#039;A text selector&#039;&#039;&#039;; similar to a selector but those are the elements that returns an area of the DOM, they are useful in steps following the format &#039;&#039;&#039;... in the &amp;quot;Community finder&amp;quot; &amp;quot;block&amp;quot;&#039;&#039;&#039; where you are clicking or looking for some text inside a specific area. In the &#039;Acceptance testing&#039; interface you can see a drop-down menu to select one of these options:&lt;br /&gt;
** dialogue - for searching a dialogue with the specified header text&lt;br /&gt;
** block - for searching a Moodle block by it&#039;s English name or it&#039;s frankenstyle name&lt;br /&gt;
** section - for searching for a section on a course page by it&#039;s title or its written out date (e.g. &amp;quot;1 January - 7 January&amp;quot;). Use &amp;quot;frontpage&amp;quot; &amp;quot;section&amp;quot; for the frontpage section if it has no title (default)&lt;br /&gt;
** activity - for searching for an activity module in a course list by it&#039;s title&lt;br /&gt;
** region - for searching a Moodle page region with that id, in fact it works with all the page&#039;s ids&lt;br /&gt;
** table_row - for searching a table row which contains the specified text&lt;br /&gt;
** table - for searching a table by its id or caption&lt;br /&gt;
** fieldset - for searching a fieldset by it&#039;s id or legend&lt;br /&gt;
** css_element - for searching an element by its CSS selector&lt;br /&gt;
** xpath_element - for searching an element by its XPath&lt;br /&gt;
&lt;br /&gt;
====== Checking table values ======&lt;br /&gt;
You can check if specific value exists or not in a table row/column by using:&lt;br /&gt;
* Then &amp;quot;STRING_IN_ROW&amp;quot; row &amp;quot;COLUMN_HEADER&amp;quot; column of &amp;quot;TABLE_ID&amp;quot; table should contain &amp;quot;VALUE_TO_CHECK&amp;quot;&lt;br /&gt;
* Then the following should exist in the &amp;quot;TABLE_ID&amp;quot; table:&lt;br /&gt;
    | COLUMN_HEADER1 | COLUMN_HEADER2 |&lt;br /&gt;
    | VALUE_IN_ROW_1 | VALUE_IN_ROW_1 |&lt;br /&gt;
    | VALUE_IN_ROW_2 | VALUE_IN_ROW_2 |&lt;br /&gt;
&lt;br /&gt;
==== Advanced use cases ====&lt;br /&gt;
Most of the time the usage of existing step definitions is straight forward. However, there are some exceptions were it might get complicated. Some of them are listed here:&lt;br /&gt;
&lt;br /&gt;
====== Uploading files ======&lt;br /&gt;
Note than some tests requires files to be uploaded, in this case&lt;br /&gt;
* The &#039;&#039;&#039;I upload &amp;quot;FILEPATH_STRING&amp;quot; file to &amp;quot;FILEPICKER_FIELD_STRING&amp;quot; filepicker&#039;&#039;&#039; step can be used when located in the form page&lt;br /&gt;
* The file to upload should be included along with the Moodle codebase in COMPONENTNAME/tests/fixtures/*&lt;br /&gt;
* The file to upload is specified by it&#039;s path, which should be relative to the codebase root (&#039;&#039;&#039;lib/tests/fixtures/users.csv&#039;&#039;&#039; for example) &lt;br /&gt;
* &#039;&#039;&#039;/&#039;&#039;&#039; should be used as directory separator and the file names can not include this &#039;&#039;&#039;/&#039;&#039;&#039; character as all of them would be converted to the OS-dependant directory separator to maintain the compatibility with Windows systems.&lt;br /&gt;
* The scenarios that includes files uploading should be tagged using the &#039;&#039;&#039;@_file_upload&#039;&#039;&#039; tag&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@editor @editor_atto @atto @atto_media @_file_upload&lt;br /&gt;
Feature: Add media to Atto&lt;br /&gt;
  To write rich text - I need to add media.&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given I log in as &amp;quot;admin&amp;quot;&lt;br /&gt;
    And I follow &amp;quot;Manage private files...&amp;quot;&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.webm&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.mp4&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.png&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/pretty-good-en.vtt&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/pretty-good-sv.vtt&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I click on &amp;quot;Save changes&amp;quot; &amp;quot;button&amp;quot;&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Field groups ======&lt;br /&gt;
This section describes how you can use the step definitions&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
...&lt;br /&gt;
When I set the field &amp;quot;([^&amp;quot;]|\&amp;quot;*)&amp;quot; to &amp;quot;([^&amp;quot;]|\&amp;quot;*)&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
for field groups. Examples for such field groups are the duration field or the date_time_selector. These are not displayed as one single input field within the front-end but consist of multiple input fields within one row.&lt;br /&gt;
You can access each single input field of a group using &lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
identifierOfYourField[keyOfTheSpecificInput]&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Examples would be:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
  | myDate[day]             |   21   |&lt;br /&gt;
  | myDate[month]           |   12   |&lt;br /&gt;
  | myDate[hour]            |   14   |&lt;br /&gt;
  | myDuration[number]      |   10   |&lt;br /&gt;
  | myDuration[unit]        | days   |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Relative dates ======&lt;br /&gt;
When testing plugins with deadlines, for instance for submissions, it is often necessary to set certain time values to dates relative to today.&lt;br /&gt;
You can specify a relative time enclosed within two ## blocks. For example:&lt;br /&gt;
* ## yesterday ##&lt;br /&gt;
* ## 2 days ago ##&lt;br /&gt;
You can use everything according to [http://php.net/manual/en/datetime.formats.php].&lt;br /&gt;
&lt;br /&gt;
Especially useful are the relative formats from: [http://php.net/manual/en/datetime.formats.relative.php]&lt;br /&gt;
&lt;br /&gt;
Additionally, you can specify a format you want the date to be returned into:&lt;br /&gt;
* ## yesterday ## myformat ##&lt;br /&gt;
These formats can be used as outlined in [http://php.net/manual/en/function.date.php].&lt;br /&gt;
This can be combined with the field groups:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
  | myDate[day]             |   ## yesterday ## j ##   |&lt;br /&gt;
  | myDate[month]           |   ## yesterday ## n ##   |&lt;br /&gt;
  | myDate[year]            |   ## yesterday ## Y ##   |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Good practice ==&lt;br /&gt;
&lt;br /&gt;
=== Test one thing per scenario ===&lt;br /&gt;
&lt;br /&gt;
The ideal that you should strive for, is that each scenario tests just one specific bit of functionality. Therefore, if one test fails, the scenario name should tell you exactly what the bug is. Also, any bug should cause just one scenario to fail, not lots of unrelated ones. If you can achieve this, then the idea is that it minimises the time from seeing a test fail to having fixed the bug that was detected. Of course, this ideal is not always achievable, but in my experience it is worth striving for.&lt;br /&gt;
&lt;br /&gt;
=== Set-up (Given) should not use the UI ===&lt;br /&gt;
&lt;br /&gt;
The setup is not what you are really testing here. Therefore, it should be as quick and reliable as possible. The way to achieve this is with steps like &amp;lt;tt&amp;gt;And the following &amp;quot;Thing&amp;quot; exist:&amp;lt;/tt&amp;gt; which directly insert the data into the database. If necessary, write extra steps for your plugin to setup the things you need.&lt;br /&gt;
&lt;br /&gt;
=== Don&#039;t use XPath or CSS selectors - fix you Accessibility bugs ===&lt;br /&gt;
&lt;br /&gt;
If, the only way you can identify something in the page that you want to manipulate is with a step like &amp;lt;tt&amp;gt;I set the field with xpath &amp;quot;//textarea[contains(@name, &#039;answer&#039;)]&amp;quot; to &amp;quot;frog&amp;quot;&amp;lt;/tt&amp;gt;, then this is probably the sign that you have an Accessibility bug, because Behat accesses the page very like a screen-reader user would.&lt;br /&gt;
&lt;br /&gt;
You should be able to refer to things with steps like &amp;lt;tt&amp;gt;I set the field &amp;quot;Answer&amp;quot; to &amp;quot;frog&amp;quot;&#039;&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I click on &amp;quot;True&amp;quot; &amp;quot;radio&amp;quot; in the &amp;quot;First question&amp;quot; &amp;quot;question&amp;quot;&amp;lt;/tt&amp;gt;. If not, you should probably think about fixing the accessibility bug, rather than resorting to unreadable selectors in your Behat test.&lt;br /&gt;
&lt;br /&gt;
=== When you define more steps in your plugin, make it clear they come from your plugin ===&lt;br /&gt;
&lt;br /&gt;
When defining new Step definitions in your plugin, try to make sure the step name identifies it as belonging to your plugin. So, don&#039;t make a step called &amp;lt;tt&amp;gt;I disable UI plugins&amp;lt;/tt&amp;gt;. Call it something like &amp;lt;tt&amp;gt;I disable UI plugins in the CodeRunner question type&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Writing_acceptance_tests&amp;diff=55918</id>
		<title>Writing acceptance tests</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Writing_acceptance_tests&amp;diff=55918"/>
		<updated>2019-04-15T14:08:09Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Introduction */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
This documentation gives some hints, how to write behat tests for core and for plugins. The focus of the documentation is on behat tests for plugins. They are written in some kind of natural language and describe how the front-end of moodle should behave, when a user interacts with it.&lt;br /&gt;
&lt;br /&gt;
Each test consists of a set of so called steps in a GIVEN, WHEN, THEN style:&lt;br /&gt;
* &#039;&#039;&#039;GIVEN&#039;&#039;&#039;: These steps outline the state of your moodle platform at the start of your test. Here you can create users, courses and plugin instances.&lt;br /&gt;
* &#039;&#039;&#039;WHEN&#039;&#039;&#039;: These steps usually execute the functionality of your plugin, which is under test.&lt;br /&gt;
* &#039;&#039;&#039;THEN&#039;&#039;&#039;: These steps describe the expected behaviour of your plugin. They usually check if different elements can or can&#039;t be seen.&lt;br /&gt;
This matches the standard [http://xunitpatterns.com/Four%20Phase%20Test.html Four-phase test pattern]. The fourth phase is &#039;tear-down&#039; which we don&#039;t need because Behat automatically cleans up after each test.&lt;br /&gt;
&lt;br /&gt;
To initialize and run your tests, please follow the instructions of [[Running_acceptance_test]].&lt;br /&gt;
&lt;br /&gt;
== Create you own tests ==&lt;br /&gt;
Behat tests are located within the directory tests/behat of your plugin.&lt;br /&gt;
The different tests are defined in files with the ending *.feature.&lt;br /&gt;
First, you have to define the header of your test:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@mod @mod_yourplugin @javascript&lt;br /&gt;
Feature: Here comes a description of your user story.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The tags on top of the feature description can be used to select specific test cases when running the tests.&lt;br /&gt;
The &#039;@javascript&#039; tag should only be used, if javascript is needed to execute your test. This is dependent on the step you will use in your definition.&lt;br /&gt;
Javascript tests are usually much slower than tests executed without javascript.&lt;br /&gt;
&lt;br /&gt;
Afterwards you can specify a scenario:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@javascript&lt;br /&gt;
  Scenario: Description of your scenario, which you want to test.&lt;br /&gt;
    When I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I am on &amp;quot;Course 1&amp;quot; course homepage&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Again you can define specific tags. Afterwards you write the steps, which should be executed during your test.&lt;br /&gt;
&lt;br /&gt;
==== Multiple Scenarios ====&lt;br /&gt;
You can have an arbitrary amount of scenarios within a test. Please make sure they all belong to the same feature.&lt;br /&gt;
If you have certain steps, which should be executed for every scenario of a feature, you can define them using a background:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname | category | groupmode |&lt;br /&gt;
      | Course 1 | C1        | 0        | 1         |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username | firstname | lastname | email |&lt;br /&gt;
      | teacher1 | Theo | Teacher | teacher1@example.com |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
This is usually used, to define the different &#039;&#039;&#039;GIVEN&#039;&#039;&#039; steps.&lt;br /&gt;
&lt;br /&gt;
==== Use existing steps ====&lt;br /&gt;
There are different ways how to effectively browse the available existing steps:&lt;br /&gt;
&lt;br /&gt;
====== Moodle Administration ======&lt;br /&gt;
Moodle offers within its administration menu under Site Administration &amp;gt; Development &amp;gt; Acceptance Testing a complete and searchable list of all available step definitions.&lt;br /&gt;
However, make sure you installed the behat test site first!&lt;br /&gt;
&lt;br /&gt;
====== PhpStorm ======&lt;br /&gt;
In PhpStorm or IntelliJ you can install the behat extension. Then you get auto completions within feature files, which helps a lot during behat test development.&lt;br /&gt;
&lt;br /&gt;
==== Providing values to steps ====&lt;br /&gt;
Most of the steps requires values, there are methods to provide values to steps, the method depends on the step specification.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;A string/text&#039;&#039;&#039;; is the most common case, the texts are wrapped between double quotes (&amp;quot; character) you have to replace the info about the expected value for your value; for example something like &#039;&#039;&#039;I press &amp;quot;BUTTON_STRING&amp;quot;&#039;&#039;&#039; should become &#039;&#039;&#039;I press &amp;quot;Save and return to course&amp;quot;&#039;&#039;&#039;. If you want to add a string which contains a &amp;quot; character, you can escape it with \&amp;quot;, for example &#039;&#039;&#039;I fill the &amp;quot;Name&amp;quot; field with &amp;quot;Alan alias \&amp;quot;the legend\&amp;quot;&amp;quot;&#039;&#039;&#039;. You can identify this steps because they ends with &#039;&#039;&#039;_STRING&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;A number&#039;&#039;&#039;; some steps requires numbers as values, to be more specific an undetermined number of digits from 0 to 9 (Natural numbers + 0) you can identify them because the expected value info string ends with &#039;&#039;&#039;_NUMBER&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;A table&#039;&#039;&#039;; is a relation between values, the most common use of it is to fill forms. The steps which requires tables are easily identifiable because they finish with &#039;&#039;&#039;:&#039;&#039;&#039; The steps description gives info about what the table columns must contain, for example &#039;&#039;&#039;Fills a moodle form with field/value data&#039;&#039;&#039;. Here you don&#039;t need to escape the double quotes if you want to include them as part of the value.&lt;br /&gt;
* &#039;&#039;&#039;A PyString&#039;&#039;&#039;; is a multiline string, most commonly used to fill out forms when a newline is required. Like steps with tables, steps which require PyStrings will end with &amp;quot;:&amp;quot;&lt;br /&gt;
* &#039;&#039;&#039;A field value&#039;&#039;&#039;; There are many different field types, if an argument requires a field value the expected value will depend on the field type:&lt;br /&gt;
** Text-based fields: It expects the text. This includes textareas, input type text, input type password...&lt;br /&gt;
** Checkbox: It expects 1 to check and for checked and &amp;quot;&amp;quot; to uncheck or for unchecked&lt;br /&gt;
** Select: It expects the option text or the option value. In case you interact with a multi-select you should specify the options separating them with commas. For example: &#039;&#039;&#039;option1, option2, option3&#039;&#039;&#039;&lt;br /&gt;
** Radio: The text of the radio option&lt;br /&gt;
* &#039;&#039;&#039;A selector&#039;&#039;&#039;; there are steps that can be used with different kinds of elements, for example &#039;&#039;&#039;I click on &amp;quot;User Name&amp;quot; &amp;quot;link&amp;quot;&#039;&#039;&#039; or &#039;&#039;&#039;I click on &amp;quot;User Name&amp;quot; &amp;quot;button&amp;quot;&#039;&#039;&#039; this is a closed list of elements, they always works together with another argument, where you specify the locator (eg. the link text in a link) In the &#039;Acceptance testing&#039; interface you can see a drop-down menu to select one of these options:&lt;br /&gt;
** field - for searching a field by its id, name, value or label&lt;br /&gt;
** link - for searching a link by its href, id, title, img alt or value&lt;br /&gt;
** button - for searching a button by its name, id, value, img alt or title&lt;br /&gt;
** link_or_button - for searching for both, links and buttons&lt;br /&gt;
** select - for searching a select field by its id, name or label&lt;br /&gt;
** checkbox - for searching a checkbox by its id, name, or label&lt;br /&gt;
** radio - for searching a radio button by its id, name, or label&lt;br /&gt;
** file - for searching a file input by its id, name, or label&lt;br /&gt;
** optgroup - for searching optgroup by its label&lt;br /&gt;
** option - for searching an option by its content&lt;br /&gt;
** dialogue - for searching a dialogue with the specified header text&lt;br /&gt;
** filemanager - for searching a filemanager by it&#039;s id or label&lt;br /&gt;
** block - for searching a Moodle block by it&#039;s English name or it&#039;s frankenstyle name&lt;br /&gt;
** section - for searching for a section on a course page by it&#039;s title or its written out date (e.g. &amp;quot;1 January - 7 January&amp;quot;). Use &amp;quot;frontpage&amp;quot; &amp;quot;section&amp;quot; for the frontpage section if it has no title (default)&lt;br /&gt;
** activity - for searching for an activity module in a course list by it&#039;s title&lt;br /&gt;
** region - for searching a Moodle page region with that id, in fact it works with all the page&#039;s ids&lt;br /&gt;
** table_row - for searching a table row which contains the specified text&lt;br /&gt;
** table - for searching a table by its id or caption&lt;br /&gt;
** fieldset - for searching a fieldset by it&#039;s id or legend&lt;br /&gt;
** css_element - for searching an element by its CSS selector&lt;br /&gt;
** xpath_element - for searching an element by its XPath&lt;br /&gt;
* &#039;&#039;&#039;A text selector&#039;&#039;&#039;; similar to a selector but those are the elements that returns an area of the DOM, they are useful in steps following the format &#039;&#039;&#039;... in the &amp;quot;Community finder&amp;quot; &amp;quot;block&amp;quot;&#039;&#039;&#039; where you are clicking or looking for some text inside a specific area. In the &#039;Acceptance testing&#039; interface you can see a drop-down menu to select one of these options:&lt;br /&gt;
** dialogue - for searching a dialogue with the specified header text&lt;br /&gt;
** block - for searching a Moodle block by it&#039;s English name or it&#039;s frankenstyle name&lt;br /&gt;
** section - for searching for a section on a course page by it&#039;s title or its written out date (e.g. &amp;quot;1 January - 7 January&amp;quot;). Use &amp;quot;frontpage&amp;quot; &amp;quot;section&amp;quot; for the frontpage section if it has no title (default)&lt;br /&gt;
** activity - for searching for an activity module in a course list by it&#039;s title&lt;br /&gt;
** region - for searching a Moodle page region with that id, in fact it works with all the page&#039;s ids&lt;br /&gt;
** table_row - for searching a table row which contains the specified text&lt;br /&gt;
** table - for searching a table by its id or caption&lt;br /&gt;
** fieldset - for searching a fieldset by it&#039;s id or legend&lt;br /&gt;
** css_element - for searching an element by its CSS selector&lt;br /&gt;
** xpath_element - for searching an element by its XPath&lt;br /&gt;
&lt;br /&gt;
====== Checking table values ======&lt;br /&gt;
You can check if specific value exists or not in a table row/column by using:&lt;br /&gt;
* Then &amp;quot;STRING_IN_ROW&amp;quot; row &amp;quot;COLUMN_HEADER&amp;quot; column of &amp;quot;TABLE_ID&amp;quot; table should contain &amp;quot;VALUE_TO_CHECK&amp;quot;&lt;br /&gt;
* Then the following should exist in the &amp;quot;TABLE_ID&amp;quot; table:&lt;br /&gt;
    | COLUMN_HEADER1 | COLUMN_HEADER2 |&lt;br /&gt;
    | VALUE_IN_ROW_1 | VALUE_IN_ROW_1 |&lt;br /&gt;
    | VALUE_IN_ROW_2 | VALUE_IN_ROW_2 |&lt;br /&gt;
&lt;br /&gt;
==== Advanced use cases ====&lt;br /&gt;
Most of the time the usage of existing step definitions is straight forward. However, there are some exceptions were it might get complicated. Some of them are listed here:&lt;br /&gt;
&lt;br /&gt;
====== Uploading files ======&lt;br /&gt;
Note than some tests requires files to be uploaded, in this case&lt;br /&gt;
* The &#039;&#039;&#039;I upload &amp;quot;FILEPATH_STRING&amp;quot; file to &amp;quot;FILEPICKER_FIELD_STRING&amp;quot; filepicker&#039;&#039;&#039; step can be used when located in the form page&lt;br /&gt;
* The file to upload should be included along with the Moodle codebase in COMPONENTNAME/tests/fixtures/*&lt;br /&gt;
* The file to upload is specified by it&#039;s path, which should be relative to the codebase root (&#039;&#039;&#039;lib/tests/fixtures/users.csv&#039;&#039;&#039; for example) &lt;br /&gt;
* &#039;&#039;&#039;/&#039;&#039;&#039; should be used as directory separator and the file names can not include this &#039;&#039;&#039;/&#039;&#039;&#039; character as all of them would be converted to the OS-dependant directory separator to maintain the compatibility with Windows systems.&lt;br /&gt;
* The scenarios that includes files uploading should be tagged using the &#039;&#039;&#039;@_file_upload&#039;&#039;&#039; tag&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
@editor @editor_atto @atto @atto_media @_file_upload&lt;br /&gt;
Feature: Add media to Atto&lt;br /&gt;
  To write rich text - I need to add media.&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given I log in as &amp;quot;admin&amp;quot;&lt;br /&gt;
    And I follow &amp;quot;Manage private files...&amp;quot;&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.webm&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.mp4&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/moodle-logo.png&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/pretty-good-en.vtt&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I upload &amp;quot;lib/editor/atto/tests/fixtures/pretty-good-sv.vtt&amp;quot; file to &amp;quot;Files&amp;quot; filemanager&lt;br /&gt;
    And I click on &amp;quot;Save changes&amp;quot; &amp;quot;button&amp;quot;&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Field groups ======&lt;br /&gt;
This section describes how you can use the step definitions&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
...&lt;br /&gt;
When I set the field &amp;quot;([^&amp;quot;]|\&amp;quot;*)&amp;quot; to &amp;quot;([^&amp;quot;]|\&amp;quot;*)&amp;quot;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
for field groups. Examples for such field groups are the duration field or the date_time_selector. These are not displayed as one single input field within the front-end but consist of multiple input fields within one row.&lt;br /&gt;
You can access each single input field of a group using &lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
identifierOfYourField[keyOfTheSpecificInput]&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Examples would be:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
  | myDate[day]             |   21   |&lt;br /&gt;
  | myDate[month]           |   12   |&lt;br /&gt;
  | myDate[hour]            |   14   |&lt;br /&gt;
  | myDuration[number]      |   10   |&lt;br /&gt;
  | myDuration[unit]        | days   |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====== Relative dates ======&lt;br /&gt;
When testing plugins with deadlines, for instance for submissions, it is often necessary to set certain time values to dates relative to today.&lt;br /&gt;
You can specify a relative time enclosed within two ## blocks. For example:&lt;br /&gt;
* ## yesterday ##&lt;br /&gt;
* ## 2 days ago ##&lt;br /&gt;
You can use everything according to [http://php.net/manual/en/datetime.formats.php].&lt;br /&gt;
&lt;br /&gt;
Especially useful are the relative formats from: [http://php.net/manual/en/datetime.formats.relative.php]&lt;br /&gt;
&lt;br /&gt;
Additionally, you can specify a format you want the date to be returned into:&lt;br /&gt;
* ## yesterday ## myformat ##&lt;br /&gt;
These formats can be used as outlined in [http://php.net/manual/en/function.date.php].&lt;br /&gt;
This can be combined with the field groups:&lt;br /&gt;
&amp;lt;code behat&amp;gt;&lt;br /&gt;
When I set the following fields to these values:&lt;br /&gt;
  | myDate[day]             |   ## yesterday ## j ##   |&lt;br /&gt;
  | myDate[month]           |   ## yesterday ## n ##   |&lt;br /&gt;
  | myDate[year]            |   ## yesterday ## Y ##   |&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Using_the_question_engine_from_module&amp;diff=55911</id>
		<title>Using the question engine from module</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Using_the_question_engine_from_module&amp;diff=55911"/>
		<updated>2019-04-15T10:57:41Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Template:Question_engine_2}}&lt;br /&gt;
This page explains how to use the new Moodle [[Question Engine 2|question engine]] in an activity module you are developing.&lt;br /&gt;
&lt;br /&gt;
Previous section: [[Developing_a_Question_Type|Developing a Question Type]]&lt;br /&gt;
&lt;br /&gt;
The first example of a module that uses the question engine is the quiz module. Looking at how the quiz code works will let you see a real working example of what is explained on this page. Another, much simpler example to look at is &amp;lt;tt&amp;gt;question/preview.php&amp;lt;/tt&amp;gt;, and https://github.com/moodleou/moodle-filter_embedquestion/blob/master/showquestion.php is simpler still.&lt;br /&gt;
&lt;br /&gt;
Note that all the question engine code has extensive PHP documentor comments that should explain the purpose of every class and method. This document is supposed to provide an overview of the key points to get you started. It does not attempt to duplicate all the details in the PHPdocs.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Overview==&lt;br /&gt;
&lt;br /&gt;
The key class you will be working with is &amp;lt;tt&amp;gt;question_usage_by_activity&amp;lt;/tt&amp;gt;. This tracks an attempt at some questions, for example a quiz attempt. The usage should provide all the interface methods you need to do things with the question engine. You should rarely need to dive deeper into the inner workings of the question engine than this.&lt;br /&gt;
&lt;br /&gt;
A usage is a collection of &amp;lt;tt&amp;gt;question_attempt&amp;lt;/tt&amp;gt; objects. A question attempt represents the state of the user&#039;s attempt at one question within the usage. When you add a question to the usage, you get back a &#039;&#039;&#039;slot&#039;&#039;&#039; number. This serves to index that question within the attempt. Using slot numbers like this means that the same question can be added more than once to the same usage. A lot of the usage API calls take this slot number to indicate which question attempt to operate on. Other API methods let you perform batch actions on all the question attempts within the usage.&lt;br /&gt;
&lt;br /&gt;
==Starting an attempt at some questions==&lt;br /&gt;
&lt;br /&gt;
The example code in this section all came from &amp;lt;tt&amp;gt;mod/quiz/startattempt.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Creating the usage===&lt;br /&gt;
&lt;br /&gt;
To create a new usage, ask the question engine:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$quba = question_engine::make_questions_usage_by_activity(&#039;mod_quiz&#039;, $quizobj-&amp;gt;get_context());&lt;br /&gt;
$quba-&amp;gt;set_preferred_behaviour($quiz-&amp;gt;preferredbehaviour);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You will see that the constructor takes the plugin name (using the Moodle 2.0 &#039;frankenstyle&#039; naming convention) and the context that owns the usage. Usages should be cleaned up automatically when a context is deleted or a plugin un-installed. You also have to tell the usage which behaviour should be used when creating the attempt at each question.&lt;br /&gt;
&lt;br /&gt;
===Adding questions===&lt;br /&gt;
&lt;br /&gt;
Having got a usage, you will probably then want to add some questions to it. The question data you loaded from the database (probably using the &amp;lt;tt&amp;gt;get_question_options&amp;lt;/tt&amp;gt; function) come in a form suitable for data handling like import/export, backup/restore, and so on. We need to convert that to an &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; object. That can be done using the &amp;lt;tt&amp;gt;question_bank::make_question&amp;lt;/tt&amp;gt; method. Alternatively, you could just use the &amp;lt;tt&amp;gt;question_bank::load_question&amp;lt;/tt&amp;gt; method, but that can only load one question at a time.&lt;br /&gt;
&lt;br /&gt;
Once you have a &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; object, you add it to the usage by calling the &amp;lt;tt&amp;gt;add_question&amp;lt;/tt&amp;gt; method passing the question definition, and also the score you want this question marked out of. You get back the slot number that has been assigned to this question. The slot numbers are allocated in order, starting from 1. You can rely on that numbering convention. For example, the quiz module uses the slot numbers to make sure the grade for each question lines up properly in the reports.&lt;br /&gt;
&lt;br /&gt;
Here is the applicable code from the quiz module&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
foreach ($quizobj-&amp;gt;get_questions() as $i =&amp;gt; $questiondata) {&lt;br /&gt;
    $question = question_bank::make_question($questiondata);&lt;br /&gt;
    $idstoslots[$i] = $quba-&amp;gt;add_question($question, $questiondata-&amp;gt;maxmark);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Starting the question attempts===&lt;br /&gt;
&lt;br /&gt;
The usage does not do much when the question is added. Before the user can start interacting with the question, you have to start it. You can either do that in bulk:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$quba-&amp;gt;start_all_questions();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
or you can start just one particular question using the &amp;lt;tt&amp;gt;start_question($slot)&amp;lt;/tt&amp;gt; method.&lt;br /&gt;
&lt;br /&gt;
===Saving the usage===&lt;br /&gt;
&lt;br /&gt;
So far, all these method calls have just built up a data structure in memory. If you want to keep it, you have to save it in the database. Fortunately, that is easy:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
question_engine::save_questions_usage_by_activity($quba);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The usage is stored as a row in the &amp;lt;tt&amp;gt;question_usages&amp;lt;/tt&amp;gt; table (plus rows in other tables). To get the id of the usage, call &amp;lt;tt&amp;gt;$quba-&amp;gt;get_id()&amp;lt;/tt&amp;gt; after you have saved it.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Displaying questions==&lt;br /&gt;
&lt;br /&gt;
Suppose that you wish to display a page that contains several questions from the usage. The questions to display will be the ones whose slot numbers are in an array called &amp;lt;tt&amp;gt;$slotsonpage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The example code in this section is derived from code used by &amp;lt;tt&amp;gt;mod/quiz/attempt.php&amp;lt;/tt&amp;gt;. However, the quiz code is split between &amp;lt;tt&amp;gt;mod/quiz/attempt.php&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;quiz_attempt&amp;lt;/tt&amp;gt; class in &amp;lt;tt&amp;gt;mod/quiz/attemptlib.php&amp;lt;/tt&amp;gt;. That separation means that if I used the real quiz code in this tutorial, it would be unnecessarily hard to understand. Instead, I have simplified the examples below. If you are interested, you should be able to match up what you see below with the real quiz code.&lt;br /&gt;
&lt;br /&gt;
===Loading the usage===&lt;br /&gt;
&lt;br /&gt;
At the end of the last section, you had just saved the usage to the database. To display the questions as part of a different script, they must be loaded from the database again. Once again you ask the question engine to do this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$quba = question_engine::load_questions_usage_by_activity($usageid);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The id that you pass to this method is the id you got from &amp;lt;tt&amp;gt;$quba-&amp;gt;get_id()&amp;lt;/tt&amp;gt; after saving the usage.&lt;br /&gt;
&lt;br /&gt;
===The question form===&lt;br /&gt;
&lt;br /&gt;
Within the page, all the questions have to be wrapped in a HTML &amp;lt;tt&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;form&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;/tt&amp;gt; tag. This form needs to POST to a scripts that processes the submitted data ([[#Processing_responses|see below]]).&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
echo &#039;&amp;lt;form id=&amp;quot;responseform&amp;quot; method=&amp;quot;post&amp;quot; action=&amp;quot;&#039; . $processurl .&lt;br /&gt;
        &#039;&amp;quot; enctype=&amp;quot;multipart/form-data&amp;quot; accept-charset=&amp;quot;utf-8&amp;quot;&amp;gt;&#039;, &amp;quot;\n&amp;lt;div&amp;gt;\n&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
$PAGE-&amp;gt;requires-&amp;gt;js_init_call(&#039;M.core_question_engine.init_form&#039;,&lt;br /&gt;
        array(&#039;#responseform&#039;), false, &#039;core_question_engine&#039;);&lt;br /&gt;
&lt;br /&gt;
echo &#039;&amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;slots&amp;quot; value=&amp;quot;&#039; . implode(&#039;,&#039;, $slotsonpage) . &amp;quot;\&amp;quot; /&amp;gt;\n&amp;quot;;&lt;br /&gt;
echo &#039;&amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;scrollpos&amp;quot; value=&amp;quot;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;tt&amp;gt;enctype&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;accept-charset&amp;lt;/tt&amp;gt; parameters are important. Please copy them. The form must have an id, and you must then write some JavaScript that passes that id to to the &amp;lt;tt&amp;gt;question_init_form&amp;lt;/tt&amp;gt; function. That function is defined in &amp;lt;tt&amp;gt;question/qengine.js&amp;lt;/tt&amp;gt; if you want to find out what it does and why it is important.&lt;br /&gt;
&lt;br /&gt;
You should output a hidden form field &amp;lt;tt&amp;gt;slots&amp;lt;/tt&amp;gt; that contains a comma-separated list of the slot numbers that appear on this page. That is not absolutely necessary, but it will make processing the form submission much more efficient.&lt;br /&gt;
&lt;br /&gt;
Another nicety is the &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; hidden field. Some question behaviours, for example the interactive behaviour, have submit buttons that do something to the question. When the user clicks this button, it is nice if when the page re-displays, it is scrolled to exactly the same place where it was. The question engine has JavaScript which, if it finds a &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; hidden field, will automatically save the scroll position to it when a button is clicked, and cause the page to scroll to that position when it reloads.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Outputting the questions===&lt;br /&gt;
&lt;br /&gt;
Finally, you can actually print the questions. Well, there is one more thing to take care of first. To control how the question appears, you must prepare a &amp;lt;tt&amp;gt;question_display_options&amp;lt;/tt&amp;gt; object that describes what should be visible. For example:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$options = new question_display_options();&lt;br /&gt;
$options-&amp;gt;marks = question_display_options::MAX_ONLY;&lt;br /&gt;
$options-&amp;gt;markdp = 2; // Display marks to 2 decimal places.&lt;br /&gt;
$options-&amp;gt;feedback = question_display_options::VISIBLE;&lt;br /&gt;
$options-&amp;gt;generalfeedback = question_display_options::HIDDEN;&lt;br /&gt;
// etc.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(In the quiz code, the display options actually come from the quiz settings.) Then you really are ready to output the questions&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
foreach ($slotsonpage as $displaynumber =&amp;gt; $slot) {&lt;br /&gt;
    echo $quba-&amp;gt;render_question($slot, $options, $displaynumber);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;tt&amp;gt;$displaynumber&amp;lt;/tt&amp;gt; is the &#039;question number&#039; you want printed next to each question. This is not necessarily the same as the slot number. For example, in the quiz, Description &#039;questions&#039; are not numbered, although they take up a slot. It does not have to be a single integer (once MDL-62358 is fixed) if you have some other scheme for identifiying quesitions in your activity. For example you could pass &#039;4.3.1&#039; or &#039;7 of 10&#039; if that is what your activity needs. If you don&#039;t pass in a number to display, that is, if you pass null, then the question is printed without a Question X heading.&lt;br /&gt;
&lt;br /&gt;
==Processing responses==&lt;br /&gt;
&lt;br /&gt;
Once again, the example here are inspired by real code from the quiz module, in this case &amp;lt;tt&amp;gt;mod/quiz/processattempt.php&amp;lt;/tt&amp;gt;, but simplified to remove mention of the &amp;lt;tt&amp;gt;quiz_attempt&amp;lt;/tt&amp;gt; class.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===The simple case===&lt;br /&gt;
&lt;br /&gt;
Normally, you just need to do something like:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$timenow = time();&lt;br /&gt;
$transaction = $DB-&amp;gt;start_delegated_transaction();&lt;br /&gt;
&lt;br /&gt;
$quba = question_engine::load_questions_usage_by_activity($usageid);&lt;br /&gt;
$quba-&amp;gt;process_all_actions($timenow);&lt;br /&gt;
question_engine::save_questions_usage_by_activity($quba);&lt;br /&gt;
&lt;br /&gt;
$transaction-&amp;gt;allow_commit();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Behind the scenes there is, of course, a lot going on, including the use of the &amp;lt;tt&amp;gt;slots&amp;lt;/tt&amp;gt; parameter to control which questions are processed.&lt;br /&gt;
&lt;br /&gt;
There may also be additional work you need to do. For example, in the quiz (before the &amp;lt;tt&amp;gt;commit_sql()&amp;lt;/tt&amp;gt; line) there is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
if ($attempt-&amp;gt;timefinish) {&lt;br /&gt;
    $attempt-&amp;gt;sumgrades = $quba-&amp;gt;get_total_mark();&lt;br /&gt;
}&lt;br /&gt;
$attempt-&amp;gt;timemodified = $timenow;&lt;br /&gt;
$DB-&amp;gt;update_record(&#039;quiz_attempts&#039;, $attempt);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===More selective processing===&lt;br /&gt;
&lt;br /&gt;
If you want something other than the default processing, you can use code like &amp;lt;tt&amp;gt;$submitteddata = $quba-&amp;gt;extract_responses($slot); $quba-&amp;gt;process_action($slot, $submitteddata);&amp;lt;/tt&amp;gt; or something similar.&lt;br /&gt;
&lt;br /&gt;
===Handling scrollpos===&lt;br /&gt;
&lt;br /&gt;
You will remember that when we printed the questions, we created a &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; hidden field. Presumably, after you have processed the submission, you will redirect the user back to a page to display the questions again. In that case, you need to get the &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; parameter, and add it on to the URL you redirect to:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// With the other required/optional_param calls at the start of processattempt.php.&lt;br /&gt;
$scrollpos = optional_param(&#039;scrollpos&#039;, &#039;&#039;, PARAM_RAW);&lt;br /&gt;
&lt;br /&gt;
// ... a bit later in the file&lt;br /&gt;
$nexturl = new moodle_url(/* something */);&lt;br /&gt;
if ($scrollpos !== &#039;&#039;) {&lt;br /&gt;
    $nexturl-&amp;gt;param(&#039;scrollpos&#039;, (int) $scrollpos);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ... after all the processing is done.&lt;br /&gt;
redirect($nexturl);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Reports==&lt;br /&gt;
&lt;br /&gt;
Places like the quiz reports, we need to get data about a lot of usages at the same time. There are methods on the &amp;lt;tt&amp;gt;question_engine_data_mapper&amp;lt;/tt&amp;gt; to do this. You should never access the data in the question engine tables directly.&lt;br /&gt;
&lt;br /&gt;
For example from the quiz grades report. (Again the example code has been simplified.)&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// $qubaids is an array of integer ids.&lt;br /&gt;
$qubaidscondition = new qubaid_list($qubaids);&lt;br /&gt;
&lt;br /&gt;
$dm = new question_engine_data_mapper();&lt;br /&gt;
$latesstepdata = $dm-&amp;gt;load_questions_usages_latest_steps(&lt;br /&gt;
        $qubaidscondition, $slots);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;qubaid_list&amp;lt;/tt&amp;gt; is a subclass of the &amp;lt;tt&amp;gt;qubaid_condition&amp;lt;/tt&amp;gt; class. This is an efficient way to represent a set of usage ids in a way that can be used in database queries. The other main subclass is &amp;lt;tt&amp;gt;qubaid_join&amp;lt;/tt&amp;gt;. That could be used like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$qubaidscondition = new qubaid_join(&#039;{quiz_attempts} quiza&#039;, &#039;quiza.uniqueid&#039;,&lt;br /&gt;
        &#039;quiza.quiz = :quizaquiz&#039;, array(&#039;quizaquiz&#039; =&amp;gt; $quizid))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Another example, from the quiz manual grading report is&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$dm = new question_engine_data_mapper();&lt;br /&gt;
$statesummary = $dm-&amp;gt;load_questions_usages_question_state_summary(&lt;br /&gt;
        $qubaidscondition, $slots);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Perhaps the use of the class &amp;lt;tt&amp;gt;question_engine_data_mapper&amp;lt;/tt&amp;gt; should be hidden inside a helper class &amp;lt;tt&amp;gt;question_engine_reporter&amp;lt;/tt&amp;gt;?)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Other methods==&lt;br /&gt;
&lt;br /&gt;
===Methods to help with settings UI===&lt;br /&gt;
&lt;br /&gt;
When you want to present users with a choice of question behaviour (for example, the quiz How questions behave option) use&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$options = question_engine::get_behaviour_options();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Question marks are stored in the database to a fixed number of decimal places. To get a list of possible decimal place display options, use&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$options = question_engine::get_dp_options();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Clean-up methods===&lt;br /&gt;
&lt;br /&gt;
To delete usages you no longer need:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// A single usage ...&lt;br /&gt;
question_engine::delete_questions_usage_by_activity($qubaid);&lt;br /&gt;
&lt;br /&gt;
// ... or many.&lt;br /&gt;
question_engine::delete_questions_usage_by_activities(&lt;br /&gt;
        new qubaid_join(&#039;{quiz_attempts} quiza&#039;, &#039;quiza.uniqueid&#039;,&lt;br /&gt;
        &#039;quiza.quiz = :quizaquiz&#039;, array(&#039;quizaquiz&#039; =&amp;gt; $quizid)));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Changing the score of a question in all attempts===&lt;br /&gt;
&lt;br /&gt;
Suppose you change the number of marks awarded to question 2 in the quiz, you need to change that information in every attempt at that quiz, you can do that with a call like&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
question_engine::set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Re-grading a question===&lt;br /&gt;
&lt;br /&gt;
You only need to re-grade a question when the question definition is edited, for example to change the scoring rules. If you are just changing the maximum possible score for a question, see the previous sub-section.&lt;br /&gt;
&lt;br /&gt;
This code example is adapted from &amp;lt;tt&amp;gt;mod/quiz/report/overview/report.php&amp;lt;/tt&amp;gt;. It re-grades selected slots within a quiz attempt.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$transaction = $DB-&amp;gt;start_delegated_transaction();&lt;br /&gt;
$quba = question_engine::load_questions_usage_by_activity($attempt-&amp;gt;uniqueid);&lt;br /&gt;
&lt;br /&gt;
foreach ($slots as $slot) {&lt;br /&gt;
    $oldfraction[$slot] = $quba-&amp;gt;get_question_fraction($slot);&lt;br /&gt;
    $quba-&amp;gt;regrade_question($slot);&lt;br /&gt;
    $newfraction[$slot] = $quba-&amp;gt;get_question_fraction($slot);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
question_engine::save_questions_usage_by_activity($quba);&lt;br /&gt;
$attempt-&amp;gt;sumgrades = $quba-&amp;gt;get_total_mark();&lt;br /&gt;
update_record(&#039;quiz_attempts&#039;, $attempt);&lt;br /&gt;
$transaction-&amp;gt;allow_commit();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
In the next section, [[Question Engine 2:Implementation plan|Implementation plan]] outlines how I will implement this proposal.&lt;br /&gt;
&lt;br /&gt;
* https://github.com/moodleou/moodle-filter_embedquestion/blob/master/showquestion.php is probably the simplest example code that displays a question and lets a user interact with it.&lt;br /&gt;
&lt;br /&gt;
* The PHP documenter comments that explain the purposes of every method in the question engine code.&lt;br /&gt;
* Back to [[Question_Engine_2|Question Engine 2]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Using_the_question_engine_from_module&amp;diff=55910</id>
		<title>Using the question engine from module</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Using_the_question_engine_from_module&amp;diff=55910"/>
		<updated>2019-04-15T10:56:42Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* See also */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Template:Question_engine_2}}&lt;br /&gt;
This page explains how to use the new Moodle [[Question Engine 2|question engine]] in an activity module you are developing.&lt;br /&gt;
&lt;br /&gt;
Previous section: [[Developing_a_Question_Type|Developing a Question Type]]&lt;br /&gt;
&lt;br /&gt;
The first example of a module that uses the question engine is the quiz module. Looking at how the quiz code works will let you see a real working example of what is explained on this page. Another, much simpler example to look at is &amp;lt;tt&amp;gt;question/preview.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Note that all the question engine code has extensive PHP documentor comments that should explain the purpose of every class and method. This document is supposed to provide an overview of the key points to get you started. It does not attempt to duplicate all the details in the PHPdocs.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Overview==&lt;br /&gt;
&lt;br /&gt;
The key class you will be working with is &amp;lt;tt&amp;gt;question_usage_by_activity&amp;lt;/tt&amp;gt;. This tracks an attempt at some questions, for example a quiz attempt. The usage should provide all the interface methods you need to do things with the question engine. You should rarely need to dive deeper into the inner workings of the question engine than this.&lt;br /&gt;
&lt;br /&gt;
A usage is a collection of &amp;lt;tt&amp;gt;question_attempt&amp;lt;/tt&amp;gt; objects. A question attempt represents the state of the user&#039;s attempt at one question within the usage. When you add a question to the usage, you get back a &#039;&#039;&#039;slot&#039;&#039;&#039; number. This serves to index that question within the attempt. Using slot numbers like this means that the same question can be added more than once to the same usage. A lot of the usage API calls take this slot number to indicate which question attempt to operate on. Other API methods let you perform batch actions on all the question attempts within the usage.&lt;br /&gt;
&lt;br /&gt;
==Starting an attempt at some questions==&lt;br /&gt;
&lt;br /&gt;
The example code in this section all came from &amp;lt;tt&amp;gt;mod/quiz/startattempt.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Creating the usage===&lt;br /&gt;
&lt;br /&gt;
To create a new usage, ask the question engine:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$quba = question_engine::make_questions_usage_by_activity(&#039;mod_quiz&#039;, $quizobj-&amp;gt;get_context());&lt;br /&gt;
$quba-&amp;gt;set_preferred_behaviour($quiz-&amp;gt;preferredbehaviour);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You will see that the constructor takes the plugin name (using the Moodle 2.0 &#039;frankenstyle&#039; naming convention) and the context that owns the usage. Usages should be cleaned up automatically when a context is deleted or a plugin un-installed. You also have to tell the usage which behaviour should be used when creating the attempt at each question.&lt;br /&gt;
&lt;br /&gt;
===Adding questions===&lt;br /&gt;
&lt;br /&gt;
Having got a usage, you will probably then want to add some questions to it. The question data you loaded from the database (probably using the &amp;lt;tt&amp;gt;get_question_options&amp;lt;/tt&amp;gt; function) come in a form suitable for data handling like import/export, backup/restore, and so on. We need to convert that to an &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; object. That can be done using the &amp;lt;tt&amp;gt;question_bank::make_question&amp;lt;/tt&amp;gt; method. Alternatively, you could just use the &amp;lt;tt&amp;gt;question_bank::load_question&amp;lt;/tt&amp;gt; method, but that can only load one question at a time.&lt;br /&gt;
&lt;br /&gt;
Once you have a &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; object, you add it to the usage by calling the &amp;lt;tt&amp;gt;add_question&amp;lt;/tt&amp;gt; method passing the question definition, and also the score you want this question marked out of. You get back the slot number that has been assigned to this question. The slot numbers are allocated in order, starting from 1. You can rely on that numbering convention. For example, the quiz module uses the slot numbers to make sure the grade for each question lines up properly in the reports.&lt;br /&gt;
&lt;br /&gt;
Here is the applicable code from the quiz module&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
foreach ($quizobj-&amp;gt;get_questions() as $i =&amp;gt; $questiondata) {&lt;br /&gt;
    $question = question_bank::make_question($questiondata);&lt;br /&gt;
    $idstoslots[$i] = $quba-&amp;gt;add_question($question, $questiondata-&amp;gt;maxmark);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Starting the question attempts===&lt;br /&gt;
&lt;br /&gt;
The usage does not do much when the question is added. Before the user can start interacting with the question, you have to start it. You can either do that in bulk:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$quba-&amp;gt;start_all_questions();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
or you can start just one particular question using the &amp;lt;tt&amp;gt;start_question($slot)&amp;lt;/tt&amp;gt; method.&lt;br /&gt;
&lt;br /&gt;
===Saving the usage===&lt;br /&gt;
&lt;br /&gt;
So far, all these method calls have just built up a data structure in memory. If you want to keep it, you have to save it in the database. Fortunately, that is easy:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
question_engine::save_questions_usage_by_activity($quba);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The usage is stored as a row in the &amp;lt;tt&amp;gt;question_usages&amp;lt;/tt&amp;gt; table (plus rows in other tables). To get the id of the usage, call &amp;lt;tt&amp;gt;$quba-&amp;gt;get_id()&amp;lt;/tt&amp;gt; after you have saved it.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Displaying questions==&lt;br /&gt;
&lt;br /&gt;
Suppose that you wish to display a page that contains several questions from the usage. The questions to display will be the ones whose slot numbers are in an array called &amp;lt;tt&amp;gt;$slotsonpage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The example code in this section is derived from code used by &amp;lt;tt&amp;gt;mod/quiz/attempt.php&amp;lt;/tt&amp;gt;. However, the quiz code is split between &amp;lt;tt&amp;gt;mod/quiz/attempt.php&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;quiz_attempt&amp;lt;/tt&amp;gt; class in &amp;lt;tt&amp;gt;mod/quiz/attemptlib.php&amp;lt;/tt&amp;gt;. That separation means that if I used the real quiz code in this tutorial, it would be unnecessarily hard to understand. Instead, I have simplified the examples below. If you are interested, you should be able to match up what you see below with the real quiz code.&lt;br /&gt;
&lt;br /&gt;
===Loading the usage===&lt;br /&gt;
&lt;br /&gt;
At the end of the last section, you had just saved the usage to the database. To display the questions as part of a different script, they must be loaded from the database again. Once again you ask the question engine to do this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$quba = question_engine::load_questions_usage_by_activity($usageid);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The id that you pass to this method is the id you got from &amp;lt;tt&amp;gt;$quba-&amp;gt;get_id()&amp;lt;/tt&amp;gt; after saving the usage.&lt;br /&gt;
&lt;br /&gt;
===The question form===&lt;br /&gt;
&lt;br /&gt;
Within the page, all the questions have to be wrapped in a HTML &amp;lt;tt&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;form&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;/tt&amp;gt; tag. This form needs to POST to a scripts that processes the submitted data ([[#Processing_responses|see below]]).&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
echo &#039;&amp;lt;form id=&amp;quot;responseform&amp;quot; method=&amp;quot;post&amp;quot; action=&amp;quot;&#039; . $processurl .&lt;br /&gt;
        &#039;&amp;quot; enctype=&amp;quot;multipart/form-data&amp;quot; accept-charset=&amp;quot;utf-8&amp;quot;&amp;gt;&#039;, &amp;quot;\n&amp;lt;div&amp;gt;\n&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
$PAGE-&amp;gt;requires-&amp;gt;js_init_call(&#039;M.core_question_engine.init_form&#039;,&lt;br /&gt;
        array(&#039;#responseform&#039;), false, &#039;core_question_engine&#039;);&lt;br /&gt;
&lt;br /&gt;
echo &#039;&amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;slots&amp;quot; value=&amp;quot;&#039; . implode(&#039;,&#039;, $slotsonpage) . &amp;quot;\&amp;quot; /&amp;gt;\n&amp;quot;;&lt;br /&gt;
echo &#039;&amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;scrollpos&amp;quot; value=&amp;quot;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;tt&amp;gt;enctype&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;accept-charset&amp;lt;/tt&amp;gt; parameters are important. Please copy them. The form must have an id, and you must then write some JavaScript that passes that id to to the &amp;lt;tt&amp;gt;question_init_form&amp;lt;/tt&amp;gt; function. That function is defined in &amp;lt;tt&amp;gt;question/qengine.js&amp;lt;/tt&amp;gt; if you want to find out what it does and why it is important.&lt;br /&gt;
&lt;br /&gt;
You should output a hidden form field &amp;lt;tt&amp;gt;slots&amp;lt;/tt&amp;gt; that contains a comma-separated list of the slot numbers that appear on this page. That is not absolutely necessary, but it will make processing the form submission much more efficient.&lt;br /&gt;
&lt;br /&gt;
Another nicety is the &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; hidden field. Some question behaviours, for example the interactive behaviour, have submit buttons that do something to the question. When the user clicks this button, it is nice if when the page re-displays, it is scrolled to exactly the same place where it was. The question engine has JavaScript which, if it finds a &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; hidden field, will automatically save the scroll position to it when a button is clicked, and cause the page to scroll to that position when it reloads.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Outputting the questions===&lt;br /&gt;
&lt;br /&gt;
Finally, you can actually print the questions. Well, there is one more thing to take care of first. To control how the question appears, you must prepare a &amp;lt;tt&amp;gt;question_display_options&amp;lt;/tt&amp;gt; object that describes what should be visible. For example:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$options = new question_display_options();&lt;br /&gt;
$options-&amp;gt;marks = question_display_options::MAX_ONLY;&lt;br /&gt;
$options-&amp;gt;markdp = 2; // Display marks to 2 decimal places.&lt;br /&gt;
$options-&amp;gt;feedback = question_display_options::VISIBLE;&lt;br /&gt;
$options-&amp;gt;generalfeedback = question_display_options::HIDDEN;&lt;br /&gt;
// etc.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(In the quiz code, the display options actually come from the quiz settings.) Then you really are ready to output the questions&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
foreach ($slotsonpage as $displaynumber =&amp;gt; $slot) {&lt;br /&gt;
    echo $quba-&amp;gt;render_question($slot, $options, $displaynumber);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;tt&amp;gt;$displaynumber&amp;lt;/tt&amp;gt; is the &#039;question number&#039; you want printed next to each question. This is not necessarily the same as the slot number. For example, in the quiz, Description &#039;questions&#039; are not numbered, although they take up a slot. It does not have to be a single integer (once MDL-62358 is fixed) if you have some other scheme for identifiying quesitions in your activity. For example you could pass &#039;4.3.1&#039; or &#039;7 of 10&#039; if that is what your activity needs. If you don&#039;t pass in a number to display, that is, if you pass null, then the question is printed without a Question X heading.&lt;br /&gt;
&lt;br /&gt;
==Processing responses==&lt;br /&gt;
&lt;br /&gt;
Once again, the example here are inspired by real code from the quiz module, in this case &amp;lt;tt&amp;gt;mod/quiz/processattempt.php&amp;lt;/tt&amp;gt;, but simplified to remove mention of the &amp;lt;tt&amp;gt;quiz_attempt&amp;lt;/tt&amp;gt; class.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===The simple case===&lt;br /&gt;
&lt;br /&gt;
Normally, you just need to do something like:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$timenow = time();&lt;br /&gt;
$transaction = $DB-&amp;gt;start_delegated_transaction();&lt;br /&gt;
&lt;br /&gt;
$quba = question_engine::load_questions_usage_by_activity($usageid);&lt;br /&gt;
$quba-&amp;gt;process_all_actions($timenow);&lt;br /&gt;
question_engine::save_questions_usage_by_activity($quba);&lt;br /&gt;
&lt;br /&gt;
$transaction-&amp;gt;allow_commit();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Behind the scenes there is, of course, a lot going on, including the use of the &amp;lt;tt&amp;gt;slots&amp;lt;/tt&amp;gt; parameter to control which questions are processed.&lt;br /&gt;
&lt;br /&gt;
There may also be additional work you need to do. For example, in the quiz (before the &amp;lt;tt&amp;gt;commit_sql()&amp;lt;/tt&amp;gt; line) there is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
if ($attempt-&amp;gt;timefinish) {&lt;br /&gt;
    $attempt-&amp;gt;sumgrades = $quba-&amp;gt;get_total_mark();&lt;br /&gt;
}&lt;br /&gt;
$attempt-&amp;gt;timemodified = $timenow;&lt;br /&gt;
$DB-&amp;gt;update_record(&#039;quiz_attempts&#039;, $attempt);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===More selective processing===&lt;br /&gt;
&lt;br /&gt;
If you want something other than the default processing, you can use code like &amp;lt;tt&amp;gt;$submitteddata = $quba-&amp;gt;extract_responses($slot); $quba-&amp;gt;process_action($slot, $submitteddata);&amp;lt;/tt&amp;gt; or something similar.&lt;br /&gt;
&lt;br /&gt;
===Handling scrollpos===&lt;br /&gt;
&lt;br /&gt;
You will remember that when we printed the questions, we created a &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; hidden field. Presumably, after you have processed the submission, you will redirect the user back to a page to display the questions again. In that case, you need to get the &amp;lt;tt&amp;gt;scrollpos&amp;lt;/tt&amp;gt; parameter, and add it on to the URL you redirect to:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// With the other required/optional_param calls at the start of processattempt.php.&lt;br /&gt;
$scrollpos = optional_param(&#039;scrollpos&#039;, &#039;&#039;, PARAM_RAW);&lt;br /&gt;
&lt;br /&gt;
// ... a bit later in the file&lt;br /&gt;
$nexturl = new moodle_url(/* something */);&lt;br /&gt;
if ($scrollpos !== &#039;&#039;) {&lt;br /&gt;
    $nexturl-&amp;gt;param(&#039;scrollpos&#039;, (int) $scrollpos);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ... after all the processing is done.&lt;br /&gt;
redirect($nexturl);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Reports==&lt;br /&gt;
&lt;br /&gt;
Places like the quiz reports, we need to get data about a lot of usages at the same time. There are methods on the &amp;lt;tt&amp;gt;question_engine_data_mapper&amp;lt;/tt&amp;gt; to do this. You should never access the data in the question engine tables directly.&lt;br /&gt;
&lt;br /&gt;
For example from the quiz grades report. (Again the example code has been simplified.)&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// $qubaids is an array of integer ids.&lt;br /&gt;
$qubaidscondition = new qubaid_list($qubaids);&lt;br /&gt;
&lt;br /&gt;
$dm = new question_engine_data_mapper();&lt;br /&gt;
$latesstepdata = $dm-&amp;gt;load_questions_usages_latest_steps(&lt;br /&gt;
        $qubaidscondition, $slots);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;qubaid_list&amp;lt;/tt&amp;gt; is a subclass of the &amp;lt;tt&amp;gt;qubaid_condition&amp;lt;/tt&amp;gt; class. This is an efficient way to represent a set of usage ids in a way that can be used in database queries. The other main subclass is &amp;lt;tt&amp;gt;qubaid_join&amp;lt;/tt&amp;gt;. That could be used like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$qubaidscondition = new qubaid_join(&#039;{quiz_attempts} quiza&#039;, &#039;quiza.uniqueid&#039;,&lt;br /&gt;
        &#039;quiza.quiz = :quizaquiz&#039;, array(&#039;quizaquiz&#039; =&amp;gt; $quizid))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Another example, from the quiz manual grading report is&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$dm = new question_engine_data_mapper();&lt;br /&gt;
$statesummary = $dm-&amp;gt;load_questions_usages_question_state_summary(&lt;br /&gt;
        $qubaidscondition, $slots);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Perhaps the use of the class &amp;lt;tt&amp;gt;question_engine_data_mapper&amp;lt;/tt&amp;gt; should be hidden inside a helper class &amp;lt;tt&amp;gt;question_engine_reporter&amp;lt;/tt&amp;gt;?)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Other methods==&lt;br /&gt;
&lt;br /&gt;
===Methods to help with settings UI===&lt;br /&gt;
&lt;br /&gt;
When you want to present users with a choice of question behaviour (for example, the quiz How questions behave option) use&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$options = question_engine::get_behaviour_options();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Question marks are stored in the database to a fixed number of decimal places. To get a list of possible decimal place display options, use&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$options = question_engine::get_dp_options();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Clean-up methods===&lt;br /&gt;
&lt;br /&gt;
To delete usages you no longer need:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
// A single usage ...&lt;br /&gt;
question_engine::delete_questions_usage_by_activity($qubaid);&lt;br /&gt;
&lt;br /&gt;
// ... or many.&lt;br /&gt;
question_engine::delete_questions_usage_by_activities(&lt;br /&gt;
        new qubaid_join(&#039;{quiz_attempts} quiza&#039;, &#039;quiza.uniqueid&#039;,&lt;br /&gt;
        &#039;quiza.quiz = :quizaquiz&#039;, array(&#039;quizaquiz&#039; =&amp;gt; $quizid)));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Changing the score of a question in all attempts===&lt;br /&gt;
&lt;br /&gt;
Suppose you change the number of marks awarded to question 2 in the quiz, you need to change that information in every attempt at that quiz, you can do that with a call like&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
question_engine::set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Re-grading a question===&lt;br /&gt;
&lt;br /&gt;
You only need to re-grade a question when the question definition is edited, for example to change the scoring rules. If you are just changing the maximum possible score for a question, see the previous sub-section.&lt;br /&gt;
&lt;br /&gt;
This code example is adapted from &amp;lt;tt&amp;gt;mod/quiz/report/overview/report.php&amp;lt;/tt&amp;gt;. It re-grades selected slots within a quiz attempt.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$transaction = $DB-&amp;gt;start_delegated_transaction();&lt;br /&gt;
$quba = question_engine::load_questions_usage_by_activity($attempt-&amp;gt;uniqueid);&lt;br /&gt;
&lt;br /&gt;
foreach ($slots as $slot) {&lt;br /&gt;
    $oldfraction[$slot] = $quba-&amp;gt;get_question_fraction($slot);&lt;br /&gt;
    $quba-&amp;gt;regrade_question($slot);&lt;br /&gt;
    $newfraction[$slot] = $quba-&amp;gt;get_question_fraction($slot);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
question_engine::save_questions_usage_by_activity($quba);&lt;br /&gt;
$attempt-&amp;gt;sumgrades = $quba-&amp;gt;get_total_mark();&lt;br /&gt;
update_record(&#039;quiz_attempts&#039;, $attempt);&lt;br /&gt;
$transaction-&amp;gt;allow_commit();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
In the next section, [[Question Engine 2:Implementation plan|Implementation plan]] outlines how I will implement this proposal.&lt;br /&gt;
&lt;br /&gt;
* https://github.com/moodleou/moodle-filter_embedquestion/blob/master/showquestion.php is probably the simplest example code that displays a question and lets a user interact with it.&lt;br /&gt;
&lt;br /&gt;
* The PHP documenter comments that explain the purposes of every method in the question engine code.&lt;br /&gt;
* Back to [[Question_Engine_2|Question Engine 2]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55881</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=55881"/>
		<updated>2019-04-03T14:02:16Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* core-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 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;
==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, 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. 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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55874</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=55874"/>
		<updated>2019-04-03T13:34:40Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Step by step example */&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 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;
==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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Question_types&amp;diff=55864</id>
		<title>Question types</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Question_types&amp;diff=55864"/>
		<updated>2019-04-01T13:29:03Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Unit tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Template:Question_engine_2}}&lt;br /&gt;
=Question type plug in development=&lt;br /&gt;
This page explains how to write a question type that works with the new Moodle [[Question Engine 2|question engine]], the question engine introduced with Moodle 2.1 .&lt;br /&gt;
&lt;br /&gt;
For instructions on making a question type for versions of Moodle prior to Moodle 2.1, see [[Question_type_plugin_how_to]].&lt;br /&gt;
&lt;br /&gt;
Previous section: [[Developing a Question Behaviour|Developing a Question Behaviour]]&lt;br /&gt;
&lt;br /&gt;
==Existing question type plug ins are helpful guides==&lt;br /&gt;
&lt;br /&gt;
To learn to write question types, you are highly encouraged to read the code of some of the existing question types included in the Moodle code base, also there are many more examples of question types in the [https://github.com/moodleou/ Open University github repository] and even more examples of question types can be found in [https://moodle.org/plugins/browse.php?list=category&amp;amp;id=29 the question type category of the Moodle plugins database].&lt;br /&gt;
&lt;br /&gt;
===Existing question types and some of their features===&lt;br /&gt;
&lt;br /&gt;
Below is a list of question types with their features, it is a good idea to examine the code of these question types especially ones that have features you may need for your new question type.&lt;br /&gt;
&lt;br /&gt;
If your question type will be GPLed then of course you can reuse the code in these question types either by cut and paste or by inheriting code from these question types.&lt;br /&gt;
&lt;br /&gt;
* Questions included in Moodle&lt;br /&gt;
** true false, short answer&lt;br /&gt;
*** simplest question types&lt;br /&gt;
** Essay question type (essay)&lt;br /&gt;
*** manually not automatically graded.&lt;br /&gt;
** Calculated question &lt;br /&gt;
*** Uses a multipage question editing form&lt;br /&gt;
** Multiple choice (multichoice)&lt;br /&gt;
*** there is actually no reason why you should only have a single &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; class for your question type. You may decide, depending on the question options, to use different definition classes where question options cause questions to behave in different ways. The multiple-choice question type does this, using different definition and renderer classes for single-response and multiple-response questions (but with common subclasses). This is possible because your question type can override the &amp;lt;tt&amp;gt;question_type::make_question_instance&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;question_definition::get_renderer&amp;lt;/tt&amp;gt; methods.&lt;br /&gt;
* Contrib questions&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_ddwtos Drag and drop into text (ddwtos)] &lt;br /&gt;
*** Use of YUI js module to add more interactivity.&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_ddmarker Drag and drop markers question type (ddmarker)]&lt;br /&gt;
*** Use of YUI js module to add more interactivity.&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_ddimageortext Drag and drop (image or text) onto image (ddimageortext)]&lt;br /&gt;
*** inherits code from Select missing words (gapselect).&lt;br /&gt;
*** also contains base classes which inherit from Select missing words (gapselect) and are used by Drag and drop markers&lt;br /&gt;
*** Use of YUI js module to add more interactivity.&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_oumultiresponse OU multiple response (oumultiresponse)]&lt;br /&gt;
*** Question with multiple parts where grade is calculated by part for multiple attempts, student is given the grade for each part for the first time they got that part right or partially right.&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_pmatch Pmatch question type (pmatch)]&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_pmatchjme Pattern match with JME editor (pmatchjme)]&lt;br /&gt;
*** Embeds a JAVA applet in the question which collects the student response and sends that to the server.&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qbehaviour_opaque Question managed by a remote engine (opaque)]&lt;br /&gt;
*** communicates with an external system by a web service protocol based on SOAP to &amp;quot;delegate the rendering of questions, the scoring of responses and the generation of feedback to a remote question engine.&amp;quot; See https://docs.moodle.org/dev/Opaque&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_gapselect Select missing words (gapselect)]&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_varnumeric Variable numeric question type (varnumeric)]&lt;br /&gt;
*** Code inherited from Variable numeric set.&lt;br /&gt;
** [https://moodle.org/plugins/view.php?plugin=qtype_varnumericset Variable numeric set question type (varnumericset)]&lt;br /&gt;
&lt;br /&gt;
==Question type plug in template==&lt;br /&gt;
&lt;br /&gt;
There is a [https://github.com/jamiepratt/moodle-qtype_TEMPLATE/ question type plug in template available on github]. See the [https://github.com/jamiepratt/moodle-qtype_TEMPLATE/blob/master/README.md README.md] file on the front page of the repository for more info about this template and how you are recommended to use it. &lt;br /&gt;
&lt;br /&gt;
==The question engine itself is well commented==&lt;br /&gt;
&lt;br /&gt;
Also note that all the question engine code has extensive PHP documenter comments that should explain the purpose of every class and method. This document is supposed to provide an overview of the key points to get you started. It does not attempt to duplicate all the details in the PHPdocs.&lt;br /&gt;
&lt;br /&gt;
This document assumes that you understand the data model, where a question attempt comprises a number of steps, and each step has some properties like a state and a mark, and an array of submitted data. [[Question_Engine_2:Overview|See the question engine overview doc for background]].&lt;br /&gt;
&lt;br /&gt;
===Changes in the question type plug in api between Moodle versions===&lt;br /&gt;
&lt;br /&gt;
[https://github.com/moodle/moodle/blob/master/question/type/upgrade.txt question/type/upgrade.txt] details the changes in the question type plug in api between Moodle versions.&lt;br /&gt;
&lt;br /&gt;
==File layout==&lt;br /&gt;
&lt;br /&gt;
Question types live in a folder in &amp;lt;tt&amp;gt;question/type&amp;lt;/tt&amp;gt;. The layout inside this folder follows the typical layout for a Moodle plugin. For example, inside the &amp;lt;tt&amp;gt;question/type/mytype/&amp;lt;/tt&amp;gt; folder we would have:&lt;br /&gt;
&lt;br /&gt;
; edit_YOURQTYPENAME_form.php : is the moodle_form used to define your question editing form. See [lib/formslib.php_Form_Definition] for details of how to use the formslib api.&lt;br /&gt;
; questiontype.php : Defines the &amp;lt;tt&amp;gt;qtype_mytype&amp;lt;/tt&amp;gt; class. This must extend &amp;lt;tt&amp;gt;question_type&amp;lt;/tt&amp;gt;.&lt;br /&gt;
; question.php : This contains the definition of the &amp;lt;tt&amp;gt;qtype_mytype_question&amp;lt;/tt&amp;gt; class, which should extend the &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; base class.&lt;br /&gt;
; renderer.php : This contains the definition of the &amp;lt;tt&amp;gt;qtype_mytype_renderer&amp;lt;/tt&amp;gt; class, which should extend the &amp;lt;tt&amp;gt;qtype_renderer&amp;lt;/tt&amp;gt; base class.&lt;br /&gt;
; tests/... : Contains the [[PHPUnit|unit tests]] and [[Acceptance_testing|Behat tests]] for this question type. You are strongly encouraged to write thorough unit tests to ensure the correctness of your question type.&lt;br /&gt;
; lang/en/qtype_myqtype.php : English language strings for this question type. You can, of course, include other languages too. The language file must define at least the strings giving the model a name etc., for example : &lt;br /&gt;
&lt;br /&gt;
    $string[&#039;pluginname&#039;] = &#039;YOURQTYPENAME&#039;;&lt;br /&gt;
    $string[&#039;pluginname_help&#039;] = &#039;Create a .. description of your question type&#039;;&lt;br /&gt;
    $string[&#039;pluginname_link&#039;] = &#039;question/type/YOURQTYPENAME&#039;;&lt;br /&gt;
    $string[&#039;pluginnameadding&#039;] = &#039;Adding a YOURQTYPENAME question&#039;;&lt;br /&gt;
    $string[&#039;pluginnameediting&#039;] = &#039;Editing a YOURQTYPENAME question&#039;;&lt;br /&gt;
    $string[&#039;pluginnamesummary&#039;] = &#039;A YOURQTYPENAME question type which allows...&#039;;&lt;br /&gt;
&lt;br /&gt;
; lib.php : There is an importnat callback here that is called by the question engine to allow access to files used by questions. As with other plugins, you can put library code here, but in modern Moodle code it is better to use the classes/ folder.&lt;br /&gt;
; db/install.xml, db/upgrade.php : For creating any database tables required, [[Installing_and_upgrading_plugin_database_tables|as normal]]. See [[#Database_tables]] below.&lt;br /&gt;
; db/access.php : Defines any capabilities required by this question type. This is very rarely needed.&lt;br /&gt;
; version.php : [[version.php|version information]] for the question type. Also you can included in here which version no of core Moodle and/or other question types this module requires. Bumping up the question version will trigger the appropriate code in upgrade.php to be run when someone accesses {yourwwwroot}/admin/.&lt;br /&gt;
; styles.css : contains the styles used by your question type. You should preface all your selectors with .que.YOURQTYPENAME so that the styles are only applied to your question type which is wrapped in a div with class &#039;que&#039; and &#039;YOURQTYPENAME&#039;.&lt;br /&gt;
; pix/icon.svg: is the 16 * 16 px icon that appears next to your question type in the question type selection panel. (Can aslo be .png or .gif.)&lt;br /&gt;
; backup/moodle2/ : contains the files to implement backup and restore for your question type.&lt;br /&gt;
; classes/ : As with any Moodle plugin, you can put any extra classes you want to use to organise your code in here, and they will be [[Automatic_class_loading|auto-loaded]].&lt;br /&gt;
; settings.php (optional) : Admin menu settings that are for every question of this type. See [[Admin settings]] for info on how to add a settings page for your question type and the examples in some question types.&lt;br /&gt;
; amd/ : any [[Javascript_Modules|JavaScript modules]] your question type needs. (Old YUI-style JavaScript in yui/ or module.js would still work, but is not recommended.)&lt;br /&gt;
&lt;br /&gt;
==Database tables==&lt;br /&gt;
&lt;br /&gt;
Note that question types should only have database tables for the question definition. All runtime data is managed by the question engine.&lt;br /&gt;
&lt;br /&gt;
Question definitions should use the core table &amp;lt;tt&amp;gt;question&amp;lt;/tt&amp;gt;, and can use the core table &amp;lt;tt&amp;gt;question_answers&amp;lt;/tt&amp;gt;. You only need to define database tables if your question definition requires extra information that cannot be stored in either of these.&lt;br /&gt;
&lt;br /&gt;
==Question type and question definition classes==&lt;br /&gt;
&lt;br /&gt;
===The question definition class is in question.php===&lt;br /&gt;
&lt;br /&gt;
This holds the definition of a particular question of this type. For example, an object of type qtype_shortanswer_question is a short-answer question instance. If you load three short-answer questions from the question bank, then you will get three instances of that class. This class is not just the question definition, it can also track the current state of a question as a student attempts it. For example, for a multiple-choice question, the class will store what order the choices have been randomly shuffled into for that student. To put it another way, the question_definition often works with a particular question_attempt.&lt;br /&gt;
&lt;br /&gt;
===The question type class is in questiontype.php===&lt;br /&gt;
&lt;br /&gt;
In contrast, the question type class represents the question type. For example, the qtype_shortanswer class represents the &#039;short answer&#039; type of question, and there will normally only be a single instance of this class. It has several responsiblities, including providing meta-data about this question type (e.g. public function name()). It knows how to load, save and delete questions of this type to and from the database (get_question_options and save_question_options, delete_question) and hence how to construct instances of the qtype_shortanswer_question class. It provides methods to help with editing questions of this type. It can also provide the implmentation for import and export in various formats.&lt;br /&gt;
&lt;br /&gt;
==Renderer class==&lt;br /&gt;
&lt;br /&gt;
Renderers are all about generating HTML output. The overall output of questions is controlled by the &amp;lt;tt&amp;gt;core_question_renderer::question(...)&amp;lt;/tt&amp;gt;, which in turn calls other methods of itself, the question type renderer and the behaviour renderer, to generate the various bits of output. See [[Question_Engine_2:Overview#What_are_the_parts_of_a_question.3F|this overview of the parts of a question]].&lt;br /&gt;
&lt;br /&gt;
Once again, in what follows, I only list the methods your are most likely to want to override.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===formulation_and_controls===&lt;br /&gt;
&lt;br /&gt;
This displays the question text, and the controls the student will use to input their response. Some question types choose to display some feedback embedded in this area, for example ticks and crosses next to parts of the student&#039;s response.&lt;br /&gt;
&lt;br /&gt;
The output code must respect the &amp;lt;tt&amp;gt;question_display_options&amp;lt;/tt&amp;gt; and only reveal as much information as the calling code wants revealed. For example, the teacher creating a quiz may not want the correct answer revealed until after the quiz close date. Also, when outputting the controls, you need to respect the &amp;lt;tt&amp;gt;-&amp;gt;readonly&amp;lt;/tt&amp;gt; option.&lt;br /&gt;
&lt;br /&gt;
===specific_feedback===&lt;br /&gt;
&lt;br /&gt;
Anything returned by this method is included in the &#039;outcome&#039; area of the question, it is some feedback that particularly relates to the response the student gave. This method is only called if the display options allow this to be shows, so you don&#039;t have to worry about testing that.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===correct_response===&lt;br /&gt;
&lt;br /&gt;
This would also be included in the &#039;outcome&#039; area. This should be an automatically generated (from the question definition) display of what the correct answer to the question is.&lt;br /&gt;
&lt;br /&gt;
==Unit tests==&lt;br /&gt;
&lt;br /&gt;
For a general introduction, see the [[PHPUnit|Moodle unit testing documentation]].&lt;br /&gt;
&lt;br /&gt;
The most important parts of your question type to test are the parts of the &amp;lt;tt&amp;gt;question_definition&amp;lt;/tt&amp;gt; class that process the students responses, including the compare and grade methods. The best way to start is probably to look at the tests for some of the standard question types.&lt;br /&gt;
&lt;br /&gt;
Another useful type of test for questions are &#039;walkthrough&#039; tests. These are implemented in using PHPunit, even though they are more integration tests than unit tests. There is [https://github.com/moodle/moodle/blob/master/question/engine/tests/helpers.php#L728 a detailed comment on the base class that explains more] and there are some nice examples in the core questions and also in the [https://github.com/moodleou/moodle-qtype_oumultiresponse/blob/master/tests/walkthrough_test.php OU Multi response github repository].&lt;br /&gt;
&lt;br /&gt;
Finally, it is good to have a few Behat tests, to check that everything works end-to-end. However, since Behat is much slower than unit tests, it is best to test all the details of the grading in PHPunit.&lt;br /&gt;
&lt;br /&gt;
==Manually testing a question type==&lt;br /&gt;
&lt;br /&gt;
The following provides a thorough test of a new question type. A lot of the following can now be done with [[Acceptance_testing|Behat]].&lt;br /&gt;
&lt;br /&gt;
# Create some new questions of your type, exercising each significantly different combination of options.&lt;br /&gt;
# Go back to the editing form for each question to make sure that the options were saved accurately and that you can change them and that the changes are then saved.&lt;br /&gt;
# Try typing invalid input into the editing form, and ensure it is rejected.&lt;br /&gt;
# Preview each of your new questions using a variety of question behaviours. Check the mark and feedback for a range of correct and incorrect responses.&lt;br /&gt;
# Add your questions to some quizzes, using a range of behaviours, and different numbers of questions per page.&lt;br /&gt;
# Preview those quizzes several times, entering a range of correct and incorrect responses. Note that during this testing, some useful behind-the-scenes data (e.g. question summary, the exact behaviour being used) is shown in the Technical information region, so you probably want to expand that region, and check what is going on there.&lt;br /&gt;
# Now log in as a student and take the quizzes for real.&lt;br /&gt;
# If the question type uses complex CSS or JavaScript, repeat this testing in different web browsers.&lt;br /&gt;
# View all of the quiz reports, and ensure that they correctly display the information about your questions.&lt;br /&gt;
# Backup the course, restore it as a new course, and ensure that all your questions have been copied across accurately.&lt;br /&gt;
# Export your questions from the original course to each of the import/export formats your support. Import these questions to a new question category, and ensure they have been copied accurately (within the limits to which your question type can be represented in each format).&lt;br /&gt;
# Edit the questions so that there is a link to another part of the course (e.g. a Page resource) in every place that HTML can be entered. Repeat the backup and restore test and verify that the URLs in the restored question have been updated to point to the new copy of the Page resource in the new course.&lt;br /&gt;
# Edit the question to add an image to every HTML field that supports it. Repeat the Editing, Preview, Backup/restore and Export/import tests, and verify that the images always work, and that you don&#039;t end up with broken image links.&lt;br /&gt;
# Move the category containing the questions with images to a new context in the question bank, and make sure that the image links don&#039;t break.&lt;br /&gt;
# Check that all the unit tests for your question type pass.&lt;br /&gt;
# Run code-checker (https://moodle.org/plugins/view.php?plugin=local_codechecker) over your plugin to check the coding style.&lt;br /&gt;
# Check your question type against Accessibility guidelines, for example the [http://www.w3.org/WAI/intro/wcag.php Web Content Accessibility Guidelines].&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
Next section: [[Using_the_question_engine_from_module|Using the question engine from a module]].&lt;br /&gt;
&lt;br /&gt;
* The PHP documenter comments that explain the purposes of every method in the question engine code.&lt;br /&gt;
* Back to [[Question_Engine_2|Question Engine 2]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Plugins]]&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55854</id>
		<title>Making changes show up during development</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55854"/>
		<updated>2019-03-29T15:23:25Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Language strings */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Sometime, when you are trying to do Moodle development, you edit the code, and nothing seems to change as a result. This is because, to improve performance, Moodle now has all sorts of caching built in. When this is happening, this page will try to give you the quickest way to make your changes show up in all situation.&lt;br /&gt;
&lt;br /&gt;
== General advice ==&lt;br /&gt;
&lt;br /&gt;
# In your development site, change most of the admin settings like &#039;Cache language strings&#039;, &#039;Cache JavaScript&#039; to be suitable for development. Note that doing this will slow down your development site a bit.&lt;br /&gt;
# If in doubt, visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039; to check that the Moodle install is up-to-date.&lt;br /&gt;
# And then &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin/purgecaches.php&#039;&#039;&#039; and click the &#039;Purge all caches&#039; button. This will slow things down for the next few requests, until the caches have re-populated. Note there is also a CLI tool to purge caches.&lt;br /&gt;
# Depending on what you changed in your plugin, then in addition to the above, you may need to remember to increase the version number in the plugin&#039;s version.php, or the changes will not show up.&lt;br /&gt;
# You may have forgotten to run grunt.&lt;br /&gt;
# If you have just added a new Behat or PHPunit test, you may need to re-run admin/tool/.../cli/init.php&lt;br /&gt;
&lt;br /&gt;
The above steps are a brute force approach. They will probably work, but are probably not the fastest way. Below, we list for all the various type of change you can make, the most efficient way to make the change show up.&lt;br /&gt;
&lt;br /&gt;
== PHP ==&lt;br /&gt;
&lt;br /&gt;
After changing the PHP files (*.php), you should just need to press reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
=== Adding or removing an auto-loaded class ===&lt;br /&gt;
&lt;br /&gt;
If you get class-not-found errors, you need to visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
== JavaScript ==&lt;br /&gt;
&lt;br /&gt;
After editing the [[Javascript Modules]] source files (amd/src/*.js), if you have Administration -&amp;gt; Appearance -&amp;gt; AJAX and Javascript -&amp;gt; Cache Javascript turned off, then you should just need to reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
If JS caching is on, you need to re-run grunt. (Do you also need to purge a cache??? I don&#039;t think so.)&lt;br /&gt;
&lt;br /&gt;
== CSS ==&lt;br /&gt;
&lt;br /&gt;
After changing the CSS styles (*.css) ...&lt;br /&gt;
&lt;br /&gt;
== SCSS ==&lt;br /&gt;
&lt;br /&gt;
After changing SCSS (*.scss) files ...&lt;br /&gt;
&lt;br /&gt;
== LESS ==&lt;br /&gt;
&lt;br /&gt;
After changing LESS (*.less) files ...&lt;br /&gt;
&lt;br /&gt;
== Language strings ==&lt;br /&gt;
&lt;br /&gt;
After changing language pack strings (lang/en/type_plugin.php):&lt;br /&gt;
&lt;br /&gt;
* The best option is to turn off Admin -&amp;gt; Language -&amp;gt; Language settings -&amp;gt; Cache all language strings when doing development. Then you only need to press F5 to refresh to see your changes.&lt;br /&gt;
* Otherwise you need to Purge all caches (or just the Language strings cache) before reloading.&lt;br /&gt;
&lt;br /&gt;
== Capabilities ==&lt;br /&gt;
&lt;br /&gt;
After changing capabilities (db/access.php) ...&lt;br /&gt;
&lt;br /&gt;
== Scheduled tasks ==&lt;br /&gt;
&lt;br /&gt;
After changing scheduled tasks (db/tasks.php) you need to&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
&lt;br /&gt;
Then check that the task shows up on Admin -&amp;gt; Server -&amp;gt; Scheduled tasks.&lt;br /&gt;
&lt;br /&gt;
== Web services ==&lt;br /&gt;
&lt;br /&gt;
After changing web services (db/services.php) ...&lt;br /&gt;
&lt;br /&gt;
== Database structure ==&lt;br /&gt;
&lt;br /&gt;
After changing the database scheme (db/install.xml, db/upgrade.php) ...&lt;br /&gt;
&lt;br /&gt;
== PHPunit ==&lt;br /&gt;
&lt;br /&gt;
== Behat ==&lt;br /&gt;
&lt;br /&gt;
After you add a new *.feature file, you need to re-run&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
&lt;br /&gt;
== Mobile support for plugins ==&lt;br /&gt;
&lt;br /&gt;
If you are doing the kind of development described in [[Mobile support for plugins]], using a pre-built version of the app like https://mobileapp.moodledemo.net/.&lt;br /&gt;
&lt;br /&gt;
Note, these steps were originally written by people mostly on question type plugins. Hopefully they are generally applicable, but if they don&#039;t make sense in your case, that might be why. Please edit to improve them.&lt;br /&gt;
&lt;br /&gt;
=== General note ===&lt;br /&gt;
&lt;br /&gt;
If there are lots of red error messages appearing in the JavaScript console (but not the very common error &#039;ERROR Error: Uncaught (in promise): null...&#039; that repeats every minute and fills the console logs!) it is often necessary to clear site data and restart.&lt;br /&gt;
&lt;br /&gt;
# In browser developer tools, go to Application -&amp;gt; Clear storage.&lt;br /&gt;
# Close the browser, and re-launch.&lt;br /&gt;
&lt;br /&gt;
===Adding mobile support to a plugin for the first time===&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
# F5 in the app to restart.&lt;br /&gt;
&lt;br /&gt;
=== HTML template ===&lt;br /&gt;
&lt;br /&gt;
After changing an existing template (mobile/*.html), just pull down in the app to refresh.&lt;br /&gt;
&lt;br /&gt;
=== Client-side JavaScript ===&lt;br /&gt;
&lt;br /&gt;
After changing an existing client-side JavaScript (mobile/*.js), press F5 in the browser developer tools to restart the app.&lt;br /&gt;
&lt;br /&gt;
=== Server-side classes/output/mobile.php ===&lt;br /&gt;
&lt;br /&gt;
???&lt;br /&gt;
&lt;br /&gt;
=== Mobile CSS ===&lt;br /&gt;
&lt;br /&gt;
# Bump version number in db/mobile.php&lt;br /&gt;
# Purge caches (specifically the tool_mobile/plugininfo MUC cache)&lt;br /&gt;
# Press F5 to restart the app.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55853</id>
		<title>Making changes show up during development</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55853"/>
		<updated>2019-03-29T15:18:06Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Scheduled tasks */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Sometime, when you are trying to do Moodle development, you edit the code, and nothing seems to change as a result. This is because, to improve performance, Moodle now has all sorts of caching built in. When this is happening, this page will try to give you the quickest way to make your changes show up in all situation.&lt;br /&gt;
&lt;br /&gt;
== General advice ==&lt;br /&gt;
&lt;br /&gt;
# In your development site, change most of the admin settings like &#039;Cache language strings&#039;, &#039;Cache JavaScript&#039; to be suitable for development. Note that doing this will slow down your development site a bit.&lt;br /&gt;
# If in doubt, visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039; to check that the Moodle install is up-to-date.&lt;br /&gt;
# And then &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin/purgecaches.php&#039;&#039;&#039; and click the &#039;Purge all caches&#039; button. This will slow things down for the next few requests, until the caches have re-populated. Note there is also a CLI tool to purge caches.&lt;br /&gt;
# Depending on what you changed in your plugin, then in addition to the above, you may need to remember to increase the version number in the plugin&#039;s version.php, or the changes will not show up.&lt;br /&gt;
# You may have forgotten to run grunt.&lt;br /&gt;
# If you have just added a new Behat or PHPunit test, you may need to re-run admin/tool/.../cli/init.php&lt;br /&gt;
&lt;br /&gt;
The above steps are a brute force approach. They will probably work, but are probably not the fastest way. Below, we list for all the various type of change you can make, the most efficient way to make the change show up.&lt;br /&gt;
&lt;br /&gt;
== PHP ==&lt;br /&gt;
&lt;br /&gt;
After changing the PHP files (*.php), you should just need to press reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
=== Adding or removing an auto-loaded class ===&lt;br /&gt;
&lt;br /&gt;
If you get class-not-found errors, you need to visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
== JavaScript ==&lt;br /&gt;
&lt;br /&gt;
After editing the [[Javascript Modules]] source files (amd/src/*.js), if you have Administration -&amp;gt; Appearance -&amp;gt; AJAX and Javascript -&amp;gt; Cache Javascript turned off, then you should just need to reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
If JS caching is on, you need to re-run grunt. (Do you also need to purge a cache??? I don&#039;t think so.)&lt;br /&gt;
&lt;br /&gt;
== CSS ==&lt;br /&gt;
&lt;br /&gt;
After changing the CSS styles (*.css) ...&lt;br /&gt;
&lt;br /&gt;
== SCSS ==&lt;br /&gt;
&lt;br /&gt;
After changing SCSS (*.scss) files ...&lt;br /&gt;
&lt;br /&gt;
== LESS ==&lt;br /&gt;
&lt;br /&gt;
After changing LESS (*.less) files ...&lt;br /&gt;
&lt;br /&gt;
== Language strings ==&lt;br /&gt;
&lt;br /&gt;
After changing language pack strings (lang/en/type_plugin.php) ...&lt;br /&gt;
&lt;br /&gt;
== Capabilities ==&lt;br /&gt;
&lt;br /&gt;
After changing capabilities (db/access.php) ...&lt;br /&gt;
&lt;br /&gt;
== Scheduled tasks ==&lt;br /&gt;
&lt;br /&gt;
After changing scheduled tasks (db/tasks.php) you need to&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
&lt;br /&gt;
Then check that the task shows up on Admin -&amp;gt; Server -&amp;gt; Scheduled tasks.&lt;br /&gt;
&lt;br /&gt;
== Web services ==&lt;br /&gt;
&lt;br /&gt;
After changing web services (db/services.php) ...&lt;br /&gt;
&lt;br /&gt;
== Database structure ==&lt;br /&gt;
&lt;br /&gt;
After changing the database scheme (db/install.xml, db/upgrade.php) ...&lt;br /&gt;
&lt;br /&gt;
== PHPunit ==&lt;br /&gt;
&lt;br /&gt;
== Behat ==&lt;br /&gt;
&lt;br /&gt;
After you add a new *.feature file, you need to re-run&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
&lt;br /&gt;
== Mobile support for plugins ==&lt;br /&gt;
&lt;br /&gt;
If you are doing the kind of development described in [[Mobile support for plugins]], using a pre-built version of the app like https://mobileapp.moodledemo.net/.&lt;br /&gt;
&lt;br /&gt;
Note, these steps were originally written by people mostly on question type plugins. Hopefully they are generally applicable, but if they don&#039;t make sense in your case, that might be why. Please edit to improve them.&lt;br /&gt;
&lt;br /&gt;
=== General note ===&lt;br /&gt;
&lt;br /&gt;
If there are lots of red error messages appearing in the JavaScript console (but not the very common error &#039;ERROR Error: Uncaught (in promise): null...&#039; that repeats every minute and fills the console logs!) it is often necessary to clear site data and restart.&lt;br /&gt;
&lt;br /&gt;
# In browser developer tools, go to Application -&amp;gt; Clear storage.&lt;br /&gt;
# Close the browser, and re-launch.&lt;br /&gt;
&lt;br /&gt;
===Adding mobile support to a plugin for the first time===&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
# F5 in the app to restart.&lt;br /&gt;
&lt;br /&gt;
=== HTML template ===&lt;br /&gt;
&lt;br /&gt;
After changing an existing template (mobile/*.html), just pull down in the app to refresh.&lt;br /&gt;
&lt;br /&gt;
=== Client-side JavaScript ===&lt;br /&gt;
&lt;br /&gt;
After changing an existing client-side JavaScript (mobile/*.js), press F5 in the browser developer tools to restart the app.&lt;br /&gt;
&lt;br /&gt;
=== Server-side classes/output/mobile.php ===&lt;br /&gt;
&lt;br /&gt;
???&lt;br /&gt;
&lt;br /&gt;
=== Mobile CSS ===&lt;br /&gt;
&lt;br /&gt;
# Bump version number in db/mobile.php&lt;br /&gt;
# Purge caches (specifically the tool_mobile/plugininfo MUC cache)&lt;br /&gt;
# Press F5 to restart the app.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55827</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=55827"/>
		<updated>2019-03-27T14:36:50Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Step 1. Update the db/mobile.php file */&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. 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;]).&lt;br /&gt;
: Please only include the strings you acutally 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;
&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55824</id>
		<title>Making changes show up during development</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55824"/>
		<updated>2019-03-26T16:43:56Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: TimHunt moved page Making changes show up during Moodle development to Making changes show up during development without leaving a redirect&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Sometime, when you are trying to do Moodle development, you edit the code, and nothing seems to change as a result. This is because, to improve performance, Moodle now has all sorts of caching built in. When this is happening, this page will try to give you the quickest way to make your changes show up in all situation.&lt;br /&gt;
&lt;br /&gt;
However, this page is still quite new, so please help build it.&lt;br /&gt;
&lt;br /&gt;
==General advice==&lt;br /&gt;
&lt;br /&gt;
# In your development site, change most of the admin settings like &#039;Cache language strings&#039;, &#039;Cache JavaScript&#039; to be suitalble for development. (But note that doing this will slow down your development site.&lt;br /&gt;
# If in doubt, visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039; to check that the Moodle install is up-to-date.&lt;br /&gt;
# And then &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/vle/admin/purgecaches.php&#039;&#039;&#039; and click the &#039;Purge all caches&#039; button. This will slow things down for the next few requests, until the caches have re-populated.&lt;br /&gt;
# Depending on what you changed in your plugin, then in addition to the above, you may need to remember to increase the version number in version.php, or the changes will not show up.&lt;br /&gt;
# You may have forgotted to run grunt.&lt;br /&gt;
# If you have just added a new Behat or PHPunit test, you may need to re-run admin/tool/.../cli/init.php.&lt;br /&gt;
&lt;br /&gt;
The above steps are a brute force approach. They will probably work, but are probably not the fastest way. Below, we list for all the various type of change you can make, the most efficient way to make the change show up.&lt;br /&gt;
&lt;br /&gt;
==Changes in the Moodle and Moodle plugins==&lt;br /&gt;
&lt;br /&gt;
===Changing PHP (*.php)===&lt;br /&gt;
&lt;br /&gt;
You should just need to press reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
===Adding or removing an auto-loaded class===&lt;br /&gt;
&lt;br /&gt;
If you get class-not-found errors, you need to visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Changing JavaScript (amd/src/*.js)===&lt;br /&gt;
&lt;br /&gt;
If you have Administration -&amp;gt; Appearance -&amp;gt; AJAX and Javascript -&amp;gt; Cache Javascript turned off, then you should just need to reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
If JS caching is on, you need to re-run grunt. (Do you also need to purge a cache??? I don&#039;t think so.)&lt;br /&gt;
&lt;br /&gt;
===Changing CSS (*.css)===&lt;br /&gt;
&lt;br /&gt;
===Changing SCSS/LESS (*.scss/*.less)===&lt;br /&gt;
&lt;br /&gt;
===Changing language strings (lang/en/type_plugin.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing capabilities (db/access.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing scheduled tasks (db/tasks.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing web services (db/services.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing database strucutre (db/install.xml, db/upgrade.php)===&lt;br /&gt;
&lt;br /&gt;
===Adding a new PHPunit test===&lt;br /&gt;
&lt;br /&gt;
===Adding a new Behat test===&lt;br /&gt;
&lt;br /&gt;
You need to re-run&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/init.php&lt;br /&gt;
&lt;br /&gt;
or any newly-added *.feature file will not be found.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes in mobile support for plugins==&lt;br /&gt;
&lt;br /&gt;
If you are doing the kind of development descrived in [[Mobile support for plugins]], using a pre-built version of the app like https://mobileapp.moodledemo.net/.&lt;br /&gt;
&lt;br /&gt;
Note, these steps were originally written by people mostly on question type plugins. Hopefully they are generally applicable, but if they don&#039;t make sense in your case, that might be why. Please edit to improve them.&lt;br /&gt;
&lt;br /&gt;
===General note it you are seeing a lot of errors===&lt;br /&gt;
&lt;br /&gt;
If there are lots of red error messages appearing in the JavaScript console (but not the very common error &#039;ERROR Error: Uncaught (in promise): null...&#039; that repeats every minute and fills the console logs!) it is often necessary to clear site data and restart.&lt;br /&gt;
&lt;br /&gt;
# In browser developer tools, go to Application -&amp;gt; Clear storage.&lt;br /&gt;
# Close the browser, and re-launch.&lt;br /&gt;
&lt;br /&gt;
===Adding mobile support to a plugin for the first time===&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
# F5 in the app to restart.&lt;br /&gt;
&lt;br /&gt;
===Change in existing template (mobile/*.html)===&lt;br /&gt;
&lt;br /&gt;
Just pull down in the app to refresh.&lt;br /&gt;
&lt;br /&gt;
===Adding a new template (mobile/*.html)===&lt;br /&gt;
&lt;br /&gt;
???&lt;br /&gt;
&lt;br /&gt;
===Change to existing client-side JavaScript (mobile/*.js)&lt;br /&gt;
&lt;br /&gt;
Press F5 in the browser developer tools to restart the app.&lt;br /&gt;
&lt;br /&gt;
===New client-side JavaScript (mobile/*.js)&lt;br /&gt;
&lt;br /&gt;
===Change to server-side classes/output/mobile.php - ???&lt;br /&gt;
&lt;br /&gt;
===Changing CSS===&lt;br /&gt;
&lt;br /&gt;
# bump version number in db/mobile.php&lt;br /&gt;
# purge caches (specifically the tool_mobile/plugininfo MUC cache)&lt;br /&gt;
# Press F5 to restart the app.&lt;br /&gt;
&lt;br /&gt;
==Changes in the Moodle mobile app==&lt;br /&gt;
&lt;br /&gt;
TODO.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55823</id>
		<title>Making changes show up during development</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55823"/>
		<updated>2019-03-26T16:27:17Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Changing web services (db/services.php) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Sometime, when you are trying to do Moodle development, you edit the code, and nothing seems to change as a result. This is because, to improve performance, Moodle now has all sorts of caching built in. When this is happening, this page will try to give you the quickest way to make your changes show up in all situation.&lt;br /&gt;
&lt;br /&gt;
However, this page is still quite new, so please help build it.&lt;br /&gt;
&lt;br /&gt;
==General advice==&lt;br /&gt;
&lt;br /&gt;
# In your development site, change most of the admin settings like &#039;Cache language strings&#039;, &#039;Cache JavaScript&#039; to be suitalble for development. (But note that doing this will slow down your development site.&lt;br /&gt;
# If in doubt, visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039; to check that the Moodle install is up-to-date.&lt;br /&gt;
# And then &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/vle/admin/purgecaches.php&#039;&#039;&#039; and click the &#039;Purge all caches&#039; button. This will slow things down for the next few requests, until the caches have re-populated.&lt;br /&gt;
# Depending on what you changed in your plugin, then in addition to the above, you may need to remember to increase the version number in version.php, or the changes will not show up.&lt;br /&gt;
# You may have forgotted to run grunt.&lt;br /&gt;
# If you have just added a new Behat or PHPunit test, you may need to re-run admin/tool/.../cli/init.php.&lt;br /&gt;
&lt;br /&gt;
The above steps are a brute force approach. They will probably work, but are probably not the fastest way. Below, we list for all the various type of change you can make, the most efficient way to make the change show up.&lt;br /&gt;
&lt;br /&gt;
==Changes in the Moodle and Moodle plugins==&lt;br /&gt;
&lt;br /&gt;
===Changing PHP (*.php)===&lt;br /&gt;
&lt;br /&gt;
You should just need to press reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
===Adding or removing an auto-loaded class===&lt;br /&gt;
&lt;br /&gt;
If you get class-not-found errors, you need to visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Changing JavaScript (amd/src/*.js)===&lt;br /&gt;
&lt;br /&gt;
If you have Administration -&amp;gt; Appearance -&amp;gt; AJAX and Javascript -&amp;gt; Cache Javascript turned off, then you should just need to reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
If JS caching is on, you need to re-run grunt. (Do you also need to purge a cache??? I don&#039;t think so.)&lt;br /&gt;
&lt;br /&gt;
===Changing CSS (*.css)===&lt;br /&gt;
&lt;br /&gt;
===Changing SCSS/LESS (*.scss/*.less)===&lt;br /&gt;
&lt;br /&gt;
===Changing language strings (lang/en/type_plugin.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing capabilities (db/access.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing scheduled tasks (db/tasks.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing web services (db/services.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing database strucutre (db/install.xml, db/upgrade.php)===&lt;br /&gt;
&lt;br /&gt;
===Adding a new PHPunit test===&lt;br /&gt;
&lt;br /&gt;
===Adding a new Behat test===&lt;br /&gt;
&lt;br /&gt;
You need to re-run&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/init.php&lt;br /&gt;
&lt;br /&gt;
or any newly-added *.feature file will not be found.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes in mobile support for plugins==&lt;br /&gt;
&lt;br /&gt;
If you are doing the kind of development descrived in [[Mobile support for plugins]], using a pre-built version of the app like https://mobileapp.moodledemo.net/.&lt;br /&gt;
&lt;br /&gt;
Note, these steps were originally written by people mostly on question type plugins. Hopefully they are generally applicable, but if they don&#039;t make sense in your case, that might be why. Please edit to improve them.&lt;br /&gt;
&lt;br /&gt;
===General note it you are seeing a lot of errors===&lt;br /&gt;
&lt;br /&gt;
If there are lots of red error messages appearing in the JavaScript console (but not the very common error &#039;ERROR Error: Uncaught (in promise): null...&#039; that repeats every minute and fills the console logs!) it is often necessary to clear site data and restart.&lt;br /&gt;
&lt;br /&gt;
# In browser developer tools, go to Application -&amp;gt; Clear storage.&lt;br /&gt;
# Close the browser, and re-launch.&lt;br /&gt;
&lt;br /&gt;
===Adding mobile support to a plugin for the first time===&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
# F5 in the app to restart.&lt;br /&gt;
&lt;br /&gt;
===Change in existing template (mobile/*.html)===&lt;br /&gt;
&lt;br /&gt;
Just pull down in the app to refresh.&lt;br /&gt;
&lt;br /&gt;
===Adding a new template (mobile/*.html)===&lt;br /&gt;
&lt;br /&gt;
???&lt;br /&gt;
&lt;br /&gt;
===Change to existing client-side JavaScript (mobile/*.js)&lt;br /&gt;
&lt;br /&gt;
Press F5 in the browser developer tools to restart the app.&lt;br /&gt;
&lt;br /&gt;
===New client-side JavaScript (mobile/*.js)&lt;br /&gt;
&lt;br /&gt;
===Change to server-side classes/output/mobile.php - ???&lt;br /&gt;
&lt;br /&gt;
===Changing CSS===&lt;br /&gt;
&lt;br /&gt;
# bump version number in db/mobile.php&lt;br /&gt;
# purge caches (specifically the tool_mobile/plugininfo MUC cache)&lt;br /&gt;
# Press F5 to restart the app.&lt;br /&gt;
&lt;br /&gt;
==Changes in the Moodle mobile app==&lt;br /&gt;
&lt;br /&gt;
TODO.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55822</id>
		<title>Making changes show up during development</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Making_changes_show_up_during_development&amp;diff=55822"/>
		<updated>2019-03-26T16:26:39Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: Created page with &amp;quot;Sometime, when you are trying to do Moodle development, you edit the code, and nothing seems to change as a result. This is because, to improve performance, Moodle now has all...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Sometime, when you are trying to do Moodle development, you edit the code, and nothing seems to change as a result. This is because, to improve performance, Moodle now has all sorts of caching built in. When this is happening, this page will try to give you the quickest way to make your changes show up in all situation.&lt;br /&gt;
&lt;br /&gt;
However, this page is still quite new, so please help build it.&lt;br /&gt;
&lt;br /&gt;
==General advice==&lt;br /&gt;
&lt;br /&gt;
# In your development site, change most of the admin settings like &#039;Cache language strings&#039;, &#039;Cache JavaScript&#039; to be suitalble for development. (But note that doing this will slow down your development site.&lt;br /&gt;
# If in doubt, visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039; to check that the Moodle install is up-to-date.&lt;br /&gt;
# And then &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/vle/admin/purgecaches.php&#039;&#039;&#039; and click the &#039;Purge all caches&#039; button. This will slow things down for the next few requests, until the caches have re-populated.&lt;br /&gt;
# Depending on what you changed in your plugin, then in addition to the above, you may need to remember to increase the version number in version.php, or the changes will not show up.&lt;br /&gt;
# You may have forgotted to run grunt.&lt;br /&gt;
# If you have just added a new Behat or PHPunit test, you may need to re-run admin/tool/.../cli/init.php.&lt;br /&gt;
&lt;br /&gt;
The above steps are a brute force approach. They will probably work, but are probably not the fastest way. Below, we list for all the various type of change you can make, the most efficient way to make the change show up.&lt;br /&gt;
&lt;br /&gt;
==Changes in the Moodle and Moodle plugins==&lt;br /&gt;
&lt;br /&gt;
===Changing PHP (*.php)===&lt;br /&gt;
&lt;br /&gt;
You should just need to press reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
===Adding or removing an auto-loaded class===&lt;br /&gt;
&lt;br /&gt;
If you get class-not-found errors, you need to visit &amp;lt;nowiki&amp;gt;http://your.domain/moodle&amp;lt;/nowiki&amp;gt;&#039;&#039;&#039;/admin&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Changing JavaScript (amd/src/*.js)===&lt;br /&gt;
&lt;br /&gt;
If you have Administration -&amp;gt; Appearance -&amp;gt; AJAX and Javascript -&amp;gt; Cache Javascript turned off, then you should just need to reload (F5) in your browser.&lt;br /&gt;
&lt;br /&gt;
If JS caching is on, you need to re-run grunt. (Do you also need to purge a cache??? I don&#039;t think so.)&lt;br /&gt;
&lt;br /&gt;
===Changing CSS (*.css)===&lt;br /&gt;
&lt;br /&gt;
===Changing SCSS/LESS (*.scss/*.less)===&lt;br /&gt;
&lt;br /&gt;
===Changing language strings (lang/en/type_plugin.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing capabilities (db/access.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing scheduled tasks (db/tasks.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing web services (db/services.php)===&lt;br /&gt;
&lt;br /&gt;
===Changing database strucutre (db/install.xml, db/upgrade.php)&lt;br /&gt;
&lt;br /&gt;
===Adding a new PHPunit test===&lt;br /&gt;
&lt;br /&gt;
===Adding a new Behat test===&lt;br /&gt;
&lt;br /&gt;
You need to re-run&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/init.php&lt;br /&gt;
&lt;br /&gt;
or any newly-added *.feature file will not be found.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes in mobile support for plugins==&lt;br /&gt;
&lt;br /&gt;
If you are doing the kind of development descrived in [[Mobile support for plugins]], using a pre-built version of the app like https://mobileapp.moodledemo.net/.&lt;br /&gt;
&lt;br /&gt;
Note, these steps were originally written by people mostly on question type plugins. Hopefully they are generally applicable, but if they don&#039;t make sense in your case, that might be why. Please edit to improve them.&lt;br /&gt;
&lt;br /&gt;
===General note it you are seeing a lot of errors===&lt;br /&gt;
&lt;br /&gt;
If there are lots of red error messages appearing in the JavaScript console (but not the very common error &#039;ERROR Error: Uncaught (in promise): null...&#039; that repeats every minute and fills the console logs!) it is often necessary to clear site data and restart.&lt;br /&gt;
&lt;br /&gt;
# In browser developer tools, go to Application -&amp;gt; Clear storage.&lt;br /&gt;
# Close the browser, and re-launch.&lt;br /&gt;
&lt;br /&gt;
===Adding mobile support to a plugin for the first time===&lt;br /&gt;
&lt;br /&gt;
# Bump the plugin version number.&lt;br /&gt;
# Go to admin -&amp;gt; Notifications.&lt;br /&gt;
# F5 in the app to restart.&lt;br /&gt;
&lt;br /&gt;
===Change in existing template (mobile/*.html)===&lt;br /&gt;
&lt;br /&gt;
Just pull down in the app to refresh.&lt;br /&gt;
&lt;br /&gt;
===Adding a new template (mobile/*.html)===&lt;br /&gt;
&lt;br /&gt;
???&lt;br /&gt;
&lt;br /&gt;
===Change to existing client-side JavaScript (mobile/*.js)&lt;br /&gt;
&lt;br /&gt;
Press F5 in the browser developer tools to restart the app.&lt;br /&gt;
&lt;br /&gt;
===New client-side JavaScript (mobile/*.js)&lt;br /&gt;
&lt;br /&gt;
===Change to server-side classes/output/mobile.php - ???&lt;br /&gt;
&lt;br /&gt;
===Changing CSS===&lt;br /&gt;
&lt;br /&gt;
# bump version number in db/mobile.php&lt;br /&gt;
# purge caches (specifically the tool_mobile/plugininfo MUC cache)&lt;br /&gt;
# Press F5 to restart the app.&lt;br /&gt;
&lt;br /&gt;
==Changes in the Moodle mobile app==&lt;br /&gt;
&lt;br /&gt;
TODO.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55820</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55820"/>
		<updated>2019-03-26T15:52:13Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Useful tips */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps. (Note it is best to checked out the integration branch, as master branch does not create config.json at the moment - March 2019).&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.0&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.0&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. There are several ways to run the Ionic app and you will want to use the configuration for the way that you prefer.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking the options are:&lt;br /&gt;
# Manage the Ionic app yourself&lt;br /&gt;
# Have Behat control the lifecycle (start/stop) of the ionic app&lt;br /&gt;
&lt;br /&gt;
The recommended option is to launch the App yourself using Docker.&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
When manually launching the app yourself, you will be responsible for starting the App before Behat starts, and stopping it when you finish your testing.&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
To use this method then you need to add the following line to config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are two ways to run the app yourself:&lt;br /&gt;
# Using Docker&lt;br /&gt;
# Launching ionic yourself&lt;br /&gt;
&lt;br /&gt;
===== Using Docker (Recommended) =====&lt;br /&gt;
&lt;br /&gt;
If you are using the Docker image, you just need to start the Docker container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -p 8100:8100 moodlehq/moodlemobile2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Using a local installation =====&lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you need to add the following line to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can specify them using the &#039;aria-label&#039; attribute. You can find the value using the Chrome inspector. (Note that if you cannot locate the thing to click using on-screen text or an aria-lable, then this is often a sign that you have an accessibility bug that should be fixed.)&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text. (Again, having two buttons that are indestinguishable to Behat is probably an indication of an accessibility bug.)&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options, or look at the source in [https://github.com/moodle/moodle/blob/master/lib/tests/behat/app_behat_runtime.js lib/tests/behat/app_behat_runtime.js].&lt;br /&gt;
&lt;br /&gt;
==Useful tips==&lt;br /&gt;
&lt;br /&gt;
# Make sure you added $CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;; to your config.php.&lt;br /&gt;
# It seems that the Mobile behat tests do not like xdebug, so I turn that off in php.ini while running the tests. (Remeber to restart Apache after that.)&lt;br /&gt;
# Remember when you need to re-run php admin/tool/behat/cli/init.php from your development code base root. Make sure the message &#039;Configured app tests for version 3.6.1&#039; (or whatever version) appears.&lt;br /&gt;
# Currently (2019-03-26) things break if you try to use the master branch of the app. (One symptom is missing language strings). You need to checkout the &#039;integration&#039; branch.&lt;br /&gt;
# The message &#039;cURL request for &amp;quot;http://localhost:8100/config.json&amp;quot; failed with: Failed to connect to localhost port 8100: Connection refused&#039; means that you have not started the ionic server, or have not waited long enough (several minutes) for it to start).&lt;br /&gt;
# Message &#039;missing config.json&#039; - this should exist in your app code base/www, just re-run ionic serve -b to re-create it.&lt;br /&gt;
# Message &#039;Error The plugins required by this course could not be loaded correctly...&#039; - means either some activity on the course is not converted to moodle mobile app plugin or there is a timeout in the request to your behat site. To clear the timeout message go to mobile site in unsafe browser (localhost:8100), open the Inspector, open the Application tab, select Clear storage, press Clear site data, close Inspector, close the tab with mobile site, re-open mobile site in new tab and log in, then in a separate tab log in to your behat site 127.0.0.1/moodle as student1/student1 and make sure you can get into course 1 without the silly error.&lt;br /&gt;
# Message &#039;Fatal error: Maximum execution time of 30 seconds exceeded in...&#039; - your local site has not been updated/visited since an upgrade. Just go to your local behat site (127.0.0.1/moodle or see your address in config.php), log in as admin and run notifications (admin/), then visit a course. Do this step often to avoid timeouts!&lt;br /&gt;
# Create an unsafe browser link on your desktop. That is, a shortcut with a command like &amp;quot;C:\Program Files (x86)\Google\Chrome\Application\chrome.exe&amp;quot; --allow-file-access-from-files --disable-web-security --user-data-dir  --allow-running-insecure-content&amp;quot;&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55819</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55819"/>
		<updated>2019-03-26T15:51:44Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps. (Note it is best to checked out the integration branch, as master branch does not create config.json at the moment - March 2019).&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.0&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.0&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. There are several ways to run the Ionic app and you will want to use the configuration for the way that you prefer.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking the options are:&lt;br /&gt;
# Manage the Ionic app yourself&lt;br /&gt;
# Have Behat control the lifecycle (start/stop) of the ionic app&lt;br /&gt;
&lt;br /&gt;
The recommended option is to launch the App yourself using Docker.&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
When manually launching the app yourself, you will be responsible for starting the App before Behat starts, and stopping it when you finish your testing.&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
To use this method then you need to add the following line to config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are two ways to run the app yourself:&lt;br /&gt;
# Using Docker&lt;br /&gt;
# Launching ionic yourself&lt;br /&gt;
&lt;br /&gt;
===== Using Docker (Recommended) =====&lt;br /&gt;
&lt;br /&gt;
If you are using the Docker image, you just need to start the Docker container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -p 8100:8100 moodlehq/moodlemobile2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Using a local installation =====&lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you need to add the following line to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can specify them using the &#039;aria-label&#039; attribute. You can find the value using the Chrome inspector. (Note that if you cannot locate the thing to click using on-screen text or an aria-lable, then this is often a sign that you have an accessibility bug that should be fixed.)&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text. (Again, having two buttons that are indestinguishable to Behat is probably an indication of an accessibility bug.)&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options, or look at the source in [https://github.com/moodle/moodle/blob/master/lib/tests/behat/app_behat_runtime.js lib/tests/behat/app_behat_runtime.js].&lt;br /&gt;
&lt;br /&gt;
===Useful tips===&lt;br /&gt;
&lt;br /&gt;
# Make sure you added $CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;; to your config.php.&lt;br /&gt;
# It seems that the Mobile behat tests do not like xdebug, so I turn that off in php.ini while running the tests. (Remeber to restart Apache after that.)&lt;br /&gt;
# Remember when you need to re-run php admin/tool/behat/cli/init.php from your development code base root. Make sure the message &#039;Configured app tests for version 3.6.1&#039; (or whatever version) appears.&lt;br /&gt;
# Currently (2019-03-26) things break if you try to use the master branch of the app. (One symptom is missing language strings). You need to checkout the &#039;integration&#039; branch.&lt;br /&gt;
# The message &#039;cURL request for &amp;quot;http://localhost:8100/config.json&amp;quot; failed with: Failed to connect to localhost port 8100: Connection refused&#039; means that you have not started the ionic server, or have not waited long enough (several minutes) for it to start).&lt;br /&gt;
# Message &#039;missing config.json&#039; - this should exist in your app code base/www, just re-run ionic serve -b to re-create it.&lt;br /&gt;
# Message &#039;Error The plugins required by this course could not be loaded correctly...&#039; - means either some activity on the course is not converted to moodle mobile app plugin or there is a timeout in the request to your behat site. To clear the timeout message go to mobile site in unsafe browser (localhost:8100), open the Inspector, open the Application tab, select Clear storage, press Clear site data, close Inspector, close the tab with mobile site, re-open mobile site in new tab and log in, then in a separate tab log in to your behat site 127.0.0.1/moodle as student1/student1 and make sure you can get into course 1 without the silly error.&lt;br /&gt;
# Message &#039;Fatal error: Maximum execution time of 30 seconds exceeded in...&#039; - your local site has not been updated/visited since an upgrade. Just go to your local behat site (127.0.0.1/moodle or see your address in config.php), log in as admin and run notifications (admin/), then visit a course. Do this step often to avoid timeouts!&lt;br /&gt;
# Create an unsafe browser link on your desktop. That is, a shortcut with a command like &amp;quot;C:\Program Files (x86)\Google\Chrome\Application\chrome.exe&amp;quot; --allow-file-access-from-files --disable-web-security --user-data-dir  --allow-running-insecure-content&amp;quot;&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55811</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55811"/>
		<updated>2019-03-26T12:05:30Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Setup the environment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The structure of this page is&lt;br /&gt;
* the first part, up to the point where you get the &#039;ionic serve&#039; command to work is what you need to be able to do development on the app and test it in a browser.&lt;br /&gt;
* the second part is about how to buld a version of the app that can be run on a device.&lt;br /&gt;
* then at the end is a list of troubleshooting advice. If you encouter a problem that is not already listed, please consider adding it.&lt;br /&gt;
&lt;br /&gt;
The majority of your development will be done using a browser. You will probably on begin to use an emulator once you need to simulate a real mobile device.&lt;br /&gt;
&lt;br /&gt;
If you are just [[Mobile support for plugins|adding mobile support to plugins]], remember that most of your development can be done using the online pre-built version at https://mobileapp.moodledemo.net/ (with Chrome or Chromium). However, if you want to be able to to write [[Acceptance_testing_for_the_mobile_app|automated acceptance tests for the app]] then you need to follow this page at least as far as getting the ionic serve command to work on this page.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Just use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same nvm commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors. You only really need &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; to work if you are going to go on and build the app.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&lt;br /&gt;
&lt;br /&gt;
Congratulations! Now that you have got to the piont where the &#039;ionic serve&#039; command works, you can start doing development on the app. You only need to read the rest of the page if you want to build packaged versions of the app.&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
According to Google, this happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. It&#039;s weird that the installation process does that, it should create the platform with the right name unless it was changed manually.&lt;br /&gt;
&lt;br /&gt;
The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&lt;br /&gt;
 ionic platform remove ios&lt;br /&gt;
 ionic platform add ios&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;cordova-plugin-file&#039;&#039; version specified in config.xml is 6.0.1, for some reason the installation process installed a wrong version for that plugin. You can manually install the &#039;&#039;cordova-plugin-file&#039;&#039; plugin like this:&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove cordova-plugin-file&lt;br /&gt;
 cordova plugin add cordova-plugin-file@6.0.1&lt;br /&gt;
&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &#039;&#039;cordova-plugin-file&#039;&#039; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55810</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55810"/>
		<updated>2019-03-26T12:04:36Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The structure of this page is&lt;br /&gt;
* the first part, up to the point where you get the &#039;ionic serve&#039; command to work is what you need to be able to do development on the app and test it in a browser.&lt;br /&gt;
* the second part is about how to buld a version of the app that can be run on a device.&lt;br /&gt;
* then at the end is a list of troubleshooting advice. If you encouter a problem that is not already listed, please consider adding it.&lt;br /&gt;
&lt;br /&gt;
The majority of your development will be done using a browser. You will probably on begin to use an emulator once you need to simulate a real mobile device.&lt;br /&gt;
&lt;br /&gt;
If you are just [[Mobile support for plugins|adding mobile support to plugins]], remember that most of your development can be done using the online pre-built version at https://mobileapp.moodledemo.net/ (with Chrome or Chromium). However, if you want to be able to to write [[Acceptance_testing_for_the_mobile_app|automated acceptance tests for the app]] then you need to follow this page at least as far as getting the ionic serve command to work on this page.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Just use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same nvm commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&lt;br /&gt;
&lt;br /&gt;
Congratulations! Now that you have got to the piont where the &#039;ionic serve&#039; command works, you can start doing development on the app. You only need to read the rest of the page if you want to build packaged versions of the app.&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
According to Google, this happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. It&#039;s weird that the installation process does that, it should create the platform with the right name unless it was changed manually.&lt;br /&gt;
&lt;br /&gt;
The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&lt;br /&gt;
 ionic platform remove ios&lt;br /&gt;
 ionic platform add ios&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;cordova-plugin-file&#039;&#039; version specified in config.xml is 6.0.1, for some reason the installation process installed a wrong version for that plugin. You can manually install the &#039;&#039;cordova-plugin-file&#039;&#039; plugin like this:&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove cordova-plugin-file&lt;br /&gt;
 cordova plugin add cordova-plugin-file@6.0.1&lt;br /&gt;
&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &#039;&#039;cordova-plugin-file&#039;&#039; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55809</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55809"/>
		<updated>2019-03-26T11:57:07Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Open the app in the browser */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Just use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same nvm commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&lt;br /&gt;
&lt;br /&gt;
Congratulations! Now that you have got to the piont where the &#039;ionic serve&#039; command works, you can start doing development on the app. You only need to read the rest of the page if you want to build packaged versions of the app.&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
According to Google, this happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. It&#039;s weird that the installation process does that, it should create the platform with the right name unless it was changed manually.&lt;br /&gt;
&lt;br /&gt;
The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&lt;br /&gt;
 ionic platform remove ios&lt;br /&gt;
 ionic platform add ios&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;cordova-plugin-file&#039;&#039; version specified in config.xml is 6.0.1, for some reason the installation process installed a wrong version for that plugin. You can manually install the &#039;&#039;cordova-plugin-file&#039;&#039; plugin like this:&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove cordova-plugin-file&lt;br /&gt;
 cordova plugin add cordova-plugin-file@6.0.1&lt;br /&gt;
&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &#039;&#039;cordova-plugin-file&#039;&#039; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55807</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55807"/>
		<updated>2019-03-26T11:40:58Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Debugging tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps. (Note it is best to checked out the integration branch, as master branch does not create config.json at the moment - March 2019).&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.0&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.0&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. There are several ways to run the Ionic app and you will want to use the configuration for the way that you prefer.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking the options are:&lt;br /&gt;
# Manage the Ionic app yourself&lt;br /&gt;
# Have Behat control the lifecycle (start/stop) of the ionic app&lt;br /&gt;
&lt;br /&gt;
The recommended option is to launch the App yourself using Docker.&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
When manually launching the app yourself, you will be responsible for starting the App before Behat starts, and stopping it when you finish your testing.&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
To use this method then you need to add the following line to config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are two ways to run the app yourself:&lt;br /&gt;
# Using Docker&lt;br /&gt;
# Launching ionic yourself&lt;br /&gt;
&lt;br /&gt;
===== Using Docker (Recommended) =====&lt;br /&gt;
&lt;br /&gt;
If you are using the Docker image, you just need to start the Docker container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -p 8100:8100 moodlehq/moodlemobile2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Using a local installation =====&lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you need to add the following line to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can specify them using the &#039;aria-label&#039; attribute. You can find the value using the Chrome inspector. (Note that if you cannot locate the thing to click using on-screen text or an aria-lable, then this is often a sign that you have an accessibility bug that should be fixed.)&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text. (Again, having two buttons that are indestinguishable to Behat is probably an indication of an accessibility bug.)&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options, or look at the source in [https://github.com/moodle/moodle/blob/master/lib/tests/behat/app_behat_runtime.js lib/tests/behat/app_behat_runtime.js].&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55806</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55806"/>
		<updated>2019-03-26T11:32:04Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Actions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps. (Note it is best to checked out the integration branch, as master branch does not create config.json at the moment - March 2019).&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.0&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.0&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. There are several ways to run the Ionic app and you will want to use the configuration for the way that you prefer.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking the options are:&lt;br /&gt;
# Manage the Ionic app yourself&lt;br /&gt;
# Have Behat control the lifecycle (start/stop) of the ionic app&lt;br /&gt;
&lt;br /&gt;
The recommended option is to launch the App yourself using Docker.&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
When manually launching the app yourself, you will be responsible for starting the App before Behat starts, and stopping it when you finish your testing.&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
To use this method then you need to add the following line to config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are two ways to run the app yourself:&lt;br /&gt;
# Using Docker&lt;br /&gt;
# Launching ionic yourself&lt;br /&gt;
&lt;br /&gt;
===== Using Docker (Recommended) =====&lt;br /&gt;
&lt;br /&gt;
If you are using the Docker image, you just need to start the Docker container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -p 8100:8100 moodlehq/moodlemobile2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Using a local installation =====&lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you need to add the following line to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can specify them using the &#039;aria-label&#039; attribute. You can find the value using the Chrome inspector. (Note that if you cannot locate the thing to click using on-screen text or an aria-lable, then this is often a sign that you have an accessibility bug that should be fixed.)&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text. (Again, having two buttons that are indestinguishable to Behat is probably an indication of an accessibility bug.)&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55805</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55805"/>
		<updated>2019-03-26T11:30:16Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Actions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps. (Note it is best to checked out the integration branch, as master branch does not create config.json at the moment - March 2019).&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.0&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.0&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. There are several ways to run the Ionic app and you will want to use the configuration for the way that you prefer.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking the options are:&lt;br /&gt;
# Manage the Ionic app yourself&lt;br /&gt;
# Have Behat control the lifecycle (start/stop) of the ionic app&lt;br /&gt;
&lt;br /&gt;
The recommended option is to launch the App yourself using Docker.&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
When manually launching the app yourself, you will be responsible for starting the App before Behat starts, and stopping it when you finish your testing.&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
To use this method then you need to add the following line to config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are two ways to run the app yourself:&lt;br /&gt;
# Using Docker&lt;br /&gt;
# Launching ionic yourself&lt;br /&gt;
&lt;br /&gt;
===== Using Docker (Recommended) =====&lt;br /&gt;
&lt;br /&gt;
If you are using the Docker image, you just need to start the Docker container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -p 8100:8100 moodlehq/moodlemobile2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Using a local installation =====&lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you need to add the following line to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can specify them using the &#039;aria-label&#039; attribute. You can find the value using the Chrome inspector. (Note that if you cannot locate the thing to click using on-screen text or an aria-lable, then this is often a sign that you have an accessibility bug that should be fixed.)&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55804</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55804"/>
		<updated>2019-03-26T11:19:32Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Using a local installation = */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps. (Note it is best to checked out the integration branch, as master branch does not create config.json at the moment - March 2019).&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.0&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.0&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. There are several ways to run the Ionic app and you will want to use the configuration for the way that you prefer.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking the options are:&lt;br /&gt;
# Manage the Ionic app yourself&lt;br /&gt;
# Have Behat control the lifecycle (start/stop) of the ionic app&lt;br /&gt;
&lt;br /&gt;
The recommended option is to launch the App yourself using Docker.&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
When manually launching the app yourself, you will be responsible for starting the App before Behat starts, and stopping it when you finish your testing.&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
To use this method then you need to add the following line to config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are two ways to run the app yourself:&lt;br /&gt;
# Using Docker&lt;br /&gt;
# Launching ionic yourself&lt;br /&gt;
&lt;br /&gt;
===== Using Docker (Recommended) =====&lt;br /&gt;
&lt;br /&gt;
If you are using the Docker image, you just need to start the Docker container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -p 8100:8100 moodlehq/moodlemobile2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Using a local installation =====&lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you need to add the following line to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55796</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55796"/>
		<updated>2019-03-25T19:37:15Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Failed to install &amp;#039;cordova-plugin-file-transfer&amp;#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;. */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
I was getting this from &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
I get this when I try to run &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55795</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55795"/>
		<updated>2019-03-25T19:37:04Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* The product name change ( tag) in config.xml is not supported dynamically */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
I was getting this from &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app if you run gulp after &amp;lt;tt&amp;gt;npm run setup&amp;lt;/tt&amp;gt; has failed with this error.&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
I get this when I try to run &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55794</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55794"/>
		<updated>2019-03-25T19:36:08Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* The product name change ( tag) in config.xml is not supported dynamically */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
I was getting this from &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&lt;br /&gt;
&lt;br /&gt;
Note that this does not seem to prevent &amp;lt;tt&amp;gt;ionic --serve&amp;lt;/tt&amp;gt; from serving a working app.&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
I get this when I try to run &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55793</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55793"/>
		<updated>2019-03-25T19:30:31Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Strange NPM errors */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
To get more debug output from npm commands, see https://docs.npmjs.com/misc/config#shorthands-and-other-cli-niceties. In particular try adding &amp;lt;tt&amp;gt;--loglevel verbose&amp;lt;/tt&amp;gt; (or &amp;lt;tt&amp;gt;--loglevel info&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;--loglevel silly&amp;lt;/tt&amp;gt;) to the command-line.&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
I was getting this from &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
I get this when I try to run &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55792</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55792"/>
		<updated>2019-03-25T19:22:24Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Troubleshooting */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
&lt;br /&gt;
&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&lt;br /&gt;
&lt;br /&gt;
===Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ...===&lt;br /&gt;
&lt;br /&gt;
You need to heed the advice at https://www.npmjs.com/package/npm#installing-on-cygwin. Cygwin users are not welcome in the Node world. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle mobile development. (You don&#039;t need your Cygwin bin folder on the Windows path. It automatically gets added to the path when you lauch Cygwin bash.)&lt;br /&gt;
&lt;br /&gt;
===The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically===&lt;br /&gt;
&lt;br /&gt;
I was getting this from &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&lt;br /&gt;
&lt;br /&gt;
===Failed to install &#039;cordova-plugin-file-transfer&#039;: CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-file@4.3.3&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-file@&amp;gt;=5.0.0&amp;quot;.===&lt;br /&gt;
&lt;br /&gt;
I get this when I try to run &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt;. I have no idea why. Please help!&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55791</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55791"/>
		<updated>2019-03-25T19:11:17Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Install Node.js */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions, for example https://github.com/coreybutler/nvm-windows/issues/58#issuecomment-272608696.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55790</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55790"/>
		<updated>2019-03-25T19:10:42Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* ReferenceError: internalBinding is not defined */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== npm update check failed===&lt;br /&gt;
&lt;br /&gt;
I got the error&lt;br /&gt;
&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&lt;br /&gt;
on Windows because I installed too much as admin, and the suggested command does not work on Windows. The is to manually check the ownership of all the files in C:\Users\username\.config\configstore. In my case it was update-notifier-npm.json which got changed to be owned by Administrator.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55789</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55789"/>
		<updated>2019-03-25T19:02:52Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Open the app in the browser */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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         # or chrome or whatever browser.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55788</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55788"/>
		<updated>2019-03-25T19:02:00Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Requirements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&lt;br /&gt;
Windows tip: ingore any use of the sudo command below. Jsut use the command without it. Most things work that way, and if they don&#039;t try in a Powershell window that you have opened with &#039;Run as administrator ...&#039;.&lt;br /&gt;
&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
&lt;br /&gt;
We 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;
===Install git===&lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
===Install Node.js===&lt;br /&gt;
&lt;br /&gt;
On Linux we recommend you use [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&lt;br /&gt;
&lt;br /&gt;
 nvm install latest&lt;br /&gt;
 nvm use 11.12.0 # Or whatever number nvm install reported.&lt;br /&gt;
&lt;br /&gt;
(Exact version is probably not critical. Moodle HQ devs report using both 8.12.x and 11.12.0 as of 2019-03-25.)&lt;br /&gt;
&lt;br /&gt;
On Windows we recommend you use https://github.com/coreybutler/nvm-windows. Same mvn commands as for Linux.&lt;br /&gt;
&lt;br /&gt;
On Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
(It may seem simpler and easier to install directly from http://nodejs.org, but actually it seems to me more tricky to get that to work. If you have previously installed Node direclty, and want to switch to nvm, you need to un-install node completely before installing nvm - or Google for trouble-shooting instructions.)&lt;br /&gt;
&lt;br /&gt;
===Install ionic===&lt;br /&gt;
 npm cache clean&lt;br /&gt;
 npm install -g cordova ionic    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
===Install the npm required packages===&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;
===Windows only: Native build dependencies===&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
Warning! this installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work on a Mac. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55787</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55787"/>
		<updated>2019-03-25T18:42:14Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Setup the environment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&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;
We 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 git&#039;&#039;&#039; &lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install Node.js&#039;&#039;&#039; &lt;br /&gt;
&lt;br /&gt;
http://nodejs.org&lt;br /&gt;
&lt;br /&gt;
For Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
On Ubuntu you may be best off installing Node via [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&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    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install the npm required packages&#039;&#039;&#039;&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;
&#039;&#039;&#039;Native build dependencies for Windows&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Push notifications for Mac&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
E.g. in Mac OS X you have to run:&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;/tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55786</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=55786"/>
		<updated>2019-03-25T18:42:02Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Setup the environment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
Note: These instructions do work (give or take) for the current 3.5.x version of the Moodle Mobile app.&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;
Remember that the majority of your development can be done using the online version https://mobileapp.moodledemo.net/ (requires Chrome or Chromium browser) as indicated in [[Mobile support for plugins]]&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;
We 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 git&#039;&#039;&#039; &lt;br /&gt;
&lt;br /&gt;
https://git-scm.com/book/en/v2/Getting-Started-Installing-Git&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install Node.js&#039;&#039;&#039; &lt;br /&gt;
&lt;br /&gt;
http://nodejs.org&lt;br /&gt;
&lt;br /&gt;
For Mac users we recommend to install NodeJS via Macports.&lt;br /&gt;
&lt;br /&gt;
On Ubuntu you may be best off installing Node via [https://github.com/creationix/nvm nvm] - this lets you switch Node versions, and makes the install a bit easier than the official installation route.&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    # (If it throws an EACCESS error, run it again with sudo)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Install the npm required packages&#039;&#039;&#039;&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;
&#039;&#039;&#039;Native build dependencies for Windows&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
node-gyp requires native build tools for your platform.  If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if not]), on Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&lt;br /&gt;
 npm install --global --production windows-build-tools&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Push notifications for Mac&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Phonegap plugin push 1.9.0 requires CocoaPods to work. The installation steps can be found in https://cocoapods.org/&lt;br /&gt;
&lt;br /&gt;
E.g. in Mac OS X you have to run:&lt;br /&gt;
&lt;br /&gt;
 sudo gem install cocoapods&lt;br /&gt;
 pod setup&lt;br /&gt;
&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the .xcworkspace file, more information here: MOBILE-1970&lt;br /&gt;
&lt;br /&gt;
== Clone the app base code ==&lt;br /&gt;
&lt;br /&gt;
Clone the code base into a local directory in your computer.&lt;br /&gt;
It may be an idea to work from the integration branch rather than master.&lt;br /&gt;
 git clone https://github.com/moodlehq/moodlemobile2.git moodlemobiledirectory&lt;br /&gt;
 cd moodlemobiledirectory&lt;br /&gt;
 git checkout integration&lt;br /&gt;
&lt;br /&gt;
== Setup the environment ==&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 and /config.xml files 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;
The following command must be run in the project&#039;s root folder:&lt;br /&gt;
 npm run setup&lt;br /&gt;
&lt;br /&gt;
If this fails, you can see what it is doing by looking at the &#039;scripts&#039; section in package.json. At the moment it is doing &amp;lt;tt&amp;gt;npm install &amp;amp;&amp;amp; cordova prepare &amp;amp;&amp;amp; gulp&amp;lt;\tt&amp;gt;. That is, running three commands back-to-back, but only carrying on if the previous one succeeds completely. You can try running the three commands separately. If you do, &amp;lt;tt&amp;gt;ionic serve&amp;lt;/tt&amp;gt; (see below) may work, even if &amp;lt;tt&amp;gt;cordova prepare&amp;lt;/tt&amp;gt; gives errors.&lt;br /&gt;
&lt;br /&gt;
== Open the app in the browser ==&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;
If you don&#039;t want to open any browser you should run:&lt;br /&gt;
&lt;br /&gt;
 ionic serve -b&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;
Update project platforms:&lt;br /&gt;
&lt;br /&gt;
 ionic cordova platform remove android&lt;br /&gt;
 ionic cordova platform remove ios&lt;br /&gt;
 ionic cordova platform add android&lt;br /&gt;
 ionic cordova platform add ios&lt;br /&gt;
&lt;br /&gt;
== Updating plugins ==&lt;br /&gt;
&lt;br /&gt;
 cordova plugin remove 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 the guides below to be able to build for Android and iOS using the command line:&lt;br /&gt;
&lt;br /&gt;
Android: https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html&lt;br /&gt;
&lt;br /&gt;
iOS: https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html&lt;br /&gt;
&lt;br /&gt;
If the build fails, please run &amp;lt;code&amp;gt;cordova requirements&amp;lt;/code&amp;gt; to check that you fulfilled all requirements for the platform.&lt;br /&gt;
&lt;br /&gt;
If you get errors while building, please see the Troubleshooting section below.&lt;br /&gt;
&lt;br /&gt;
If using &#039;&#039;&#039;Ubuntu&#039;&#039;&#039; you should install the packages: gradle and libgradle-android-plugin-java (and all its dependencies) to build.&lt;br /&gt;
&lt;br /&gt;
== Compiling using AOT ==&lt;br /&gt;
&lt;br /&gt;
Angular has 2 ways of compiling: JIT and AOT. Running &amp;quot;ionic serve&amp;quot; or &amp;quot;ionic build&amp;quot; compiles using JIT by default, which is faster to compile but the app takes longer to start.&lt;br /&gt;
&lt;br /&gt;
When building for release you should always compile using AOT, otherwise the app can take too long to start in some devices. The default AOT compiling causes some issues with the database activity and the [[Mobile support for plugins]], so you have to modify a couple of files in order to make this work.&lt;br /&gt;
&lt;br /&gt;
First you need to open the file: &#039;&#039;node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js&#039;&#039;. Search the variable called &amp;quot;&#039;&#039;_NO_RESOURCE_LOADER&#039;&#039;&amp;quot;, you&#039;ll see it has a function named &amp;quot;&#039;&#039;get&#039;&#039;&amp;quot; with this line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
throw new Error(&amp;quot;No ResourceLoader implementation has been provided. Can&#039;t read the url \&amp;quot;&amp;quot; + url + &amp;quot;\&amp;quot;&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Remove that line and put this code instead:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
        url = &#039;templates/&#039; + url;&lt;br /&gt;
&lt;br /&gt;
        var resolve;&lt;br /&gt;
        var reject;&lt;br /&gt;
        var promise = new Promise(function (res, rej) {&lt;br /&gt;
            resolve = res;&lt;br /&gt;
            reject = rej;&lt;br /&gt;
        });&lt;br /&gt;
        var xhr = new XMLHttpRequest();&lt;br /&gt;
        xhr.open(&#039;GET&#039;, url, true);&lt;br /&gt;
        xhr.responseType = &#039;text&#039;;&lt;br /&gt;
        xhr.onload = function () {&lt;br /&gt;
            // responseText is the old-school way of retrieving response (supported by IE8 &amp;amp; 9)&lt;br /&gt;
            // response/responseType properties were introduced in ResourceLoader Level2 spec (supported by IE10)&lt;br /&gt;
            var response = xhr.response || xhr.responseText;&lt;br /&gt;
            // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)&lt;br /&gt;
            var status = xhr.status === 1223 ? 204 : xhr.status;&lt;br /&gt;
            // fix status code when it is 0 (0 status is undocumented).&lt;br /&gt;
            // Occurs when accessing file resources or on Android 4.1 stock browser&lt;br /&gt;
            // while retrieving files from application cache.&lt;br /&gt;
            if (status === 0) {&lt;br /&gt;
                status = response ? 200 : 0;&lt;br /&gt;
            }&lt;br /&gt;
            if (200 &amp;lt;= status &amp;amp;&amp;amp; status &amp;lt;= 300) {&lt;br /&gt;
                resolve(response);&lt;br /&gt;
            }&lt;br /&gt;
            else {&lt;br /&gt;
                reject(&amp;quot;Failed to load &amp;quot; + url);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        xhr.onerror = function () { reject(&amp;quot;Failed to load &amp;quot; + url); };&lt;br /&gt;
        xhr.send();&lt;br /&gt;
        return promise;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We tried to replace the default loader with our own implementation, but we weren&#039;t able to make the compiler work so the only solution left was to modify the default one.&lt;br /&gt;
&lt;br /&gt;
Now you need to open the file: &#039;&#039;node_modules/@ionic/app-scripts/dist/util/config.js&#039;&#039;. In that file you need to remove the &#039;&#039;context.isProd&#039;&#039; condition from the options &#039;&#039;runMinifyJs&#039;&#039; and &#039;&#039;optimizeJs&#039;&#039;. So the final code for that part should be like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
    context.runMinifyJs = [&lt;br /&gt;
        context.runMinifyJs,&lt;br /&gt;
        hasArg(&#039;--minifyJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.runMinifyCss = [&lt;br /&gt;
        context.runMinifyCss,&lt;br /&gt;
        context.isProd || hasArg(&#039;--minifyCss&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
    context.optimizeJs = [&lt;br /&gt;
        context.optimizeJs,&lt;br /&gt;
        hasArg(&#039;--optimizeJs&#039;)&lt;br /&gt;
    ].find(function (val) { return typeof val === &#039;boolean&#039;; });&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We want to compile in production mode but without optimizing and minifying Javascript because that breaks our plugins support. However, Ionic doesn&#039;t let you do that, so the only option is to do this change.&lt;br /&gt;
&lt;br /&gt;
With these changes done you can now compile using production mode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
npm run ionic:build -- --prod&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will generate the app files and put them inside &#039;&#039;www&#039;&#039; folder. If you now want to install that app in a real device you can run &amp;quot;&#039;&#039;cordova run android&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;cordova build ios&#039;&#039;&amp;quot; (please don&#039;t use &amp;quot;&#039;&#039;ionic cordova ...&#039;&#039;&amp;quot; or &amp;quot;&#039;&#039;ionic serve&#039;&#039;&amp;quot; because it will override your build files!).&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;
&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 this 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;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
  configurations.all {&lt;br /&gt;
      resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font  ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and add this code at the end:&lt;br /&gt;
&lt;br /&gt;
 android {&lt;br /&gt;
     compileSdkVersion 26&lt;br /&gt;
     buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
 }&lt;br /&gt;
&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.  ===&lt;br /&gt;
&lt;br /&gt;
1. Download Android studio - https://developer.android.com/studio/&lt;br /&gt;
&lt;br /&gt;
2. Copy the folder android-studio/plugins/android/lib/templates&lt;br /&gt;
&lt;br /&gt;
3. Paste in the folder android-sdk-folder/Sdk/tools&lt;br /&gt;
&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
&lt;br /&gt;
Open the file &#039;&#039;platforms/android/build.gradle&#039;&#039; and configure like this:&lt;br /&gt;
&lt;br /&gt;
  allprojects {&lt;br /&gt;
      repositories {&lt;br /&gt;
          jcenter()&lt;br /&gt;
          maven {&lt;br /&gt;
              url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
          }&lt;br /&gt;
      }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;tt&amp;gt;sudo apt-get install build-essential&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
&lt;br /&gt;
If you see this error during &amp;lt;tt&amp;gt;npm setup&amp;lt;/tt&amp;gt;, run &amp;lt;tt&amp;gt;mkdir www&amp;lt;/tt&amp;gt; and retry.&lt;br /&gt;
&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &#039;natives&#039; prior to 1.1.6. I fixed it using &amp;lt;tt&amp;gt;npm install natives@1.1.6&amp;lt;/tt&amp;gt;.&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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55544</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=55544"/>
		<updated>2019-02-04T16:19:39Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Moodle version requirements */&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 supportedin 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;
==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;
===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;
=====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;mmaModCertificate&amp;quot;;&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 &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; 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;
==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;
* 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>TimHunt</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55543</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=55543"/>
		<updated>2019-02-04T16:19:08Z</updated>

		<summary type="html">&lt;p&gt;TimHunt: /* Step 5. Plugin webservices, if included */&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 supportedin 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 (to be released in May) 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;
==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;
===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;
=====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;mmaModCertificate&amp;quot;;&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 &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; 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;
==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;
* 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>TimHunt</name></author>
	</entry>
</feed>