Moodle App Plugins Development Guide
Context
Since Moodle 3.1 it is possible to support different types of Moodle plugins in the Mobile app via the Remote add-ons functionality.
Remote add-ons allow a developer to add complete support to their plugins in the Mobile app, but they have some disadvantages:
- Remote add-ons are not easy to develop and test since they are required to be developed as an Angular JS/Ionic module.
- A zip file containing the plugin must be downloaded from the server to be lazy-loaded.
- It is not easy to maintain or upgrade them.
- Developers had to set up a local Mobile development environment
In order to allow plugin developers to make their plugins compatible with the app, the Mobile team has been thinking in a new way to extend the mobile app features following these premises:
- It has to be easy to develop
- It should work without developing Angular/Ionic code
- It has to be easy to maintain
- It has to be supported since Moodle 3.1 at least
- Should support all the different types of Moodle plugins supported by the app
- Should work in any type of device
- Should not require JavaScript at all, although in some cases it will be needed. In the latter case, we’ll try to simplify the required JavaScript
New approach
We published an initial draft specification, and last month we started its implementation. During the implementation process we decided to make the following changes to the initial plans in order to make developers life easier:
- There will be just one way to support plugins in the app.
- This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).
- The use of JavaScript will be optional (but some type of advanced plugins may require it)
- 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).
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.
Important notes:
- Moodle Mobile 3.5 (to be released May 2018) will be the first version of the Mobile app supporting this new type of plugins.
- Remote add-ons will have to be migrated to the new simpler way (following this documentation)
- These features will be natively supported in Moodle 3.5, but for previous versions you will need to install the Moodle Mobile Additional Features plugin.
How it works
The overall idea is to allow Moodle plugins to extend different areas in the app with just PHP server side code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.
Developers will have to:
- 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.
- 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 Ionic components so that it looks native (custom html elements) but it can be generated using mustache templates.
Let’s clarify some points:
- 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.
- Those functions will be exported via the Web Service function tool_mobile_get_content
- 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, …).
- 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..
Step by step example
In this example, we are going to update an existing plugin (Certificate activity module) that currently uses a Remote add-on. 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.
The example code can be downloaded from here (see last commit) https://github.com/jleyva/moodle-mod_certificate/commits/MOBILE-2363
Step 1. Update the db/mobile.php file
In this case, we are updating an existing file but for new plugins, you should create this new file.
$addons = array(
"mod_certificate" => array( // Plugin identifier
'handlers' => array( // Different places where the plugin will display content.
'coursecertificate' => array( // Handler unique name (alphanumeric).
'displaydata' => array(
'icon' => $CFG->wwwroot . '/mod/certificate/pix/icon.gif',
'class' => ,
),
'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the plugin)
'method' => 'mobile_course_view', // Main function in \mod_certificate\output\mobile
'offlinefunctions' => array(
'mobile_course_view' => array(),
'mobile_issues_view' => array()
) // Function that needs to be downloaded for offline.
)
),
'lang' => array( // Language strings that are used in all the handlers.
array('pluginname', 'certificate),
array('summaryofattempts', 'certificate),
array('getcertificate', 'certificate),
array('requiredtimenotmet', 'certificate),
array('viewcertificateviews', 'certificate')
),
)
);
- Plugin identifier
- A unique name for the plugin, it can be anything (there’s no need to match the module name).
- Handlers (Different places where the plugin will display content)
- A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).
- Display data
- 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.
- Delegate
- Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.
- Method
- This is the function in the Moodle component/lib.php file to be executed the first time the user clicks in the new option displayed in the app. The function should be a method of a Mobile class under the output/mobile namespace.
- Offlinefunctions
- 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.
- In our example, downloading for offline access will mean that we'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'll be able to display the certificate information even if the user is offline.
- 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).
- You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)
- Prefetching the module will also download all the files returned by the methods in these offline functions (in the files array).
- Lang
- 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.
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.
Step 2. Creating the main function
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.
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.
File contents: mod/certificate/classes/output/mobile.php
<?php
namespace mod_certificate\output;
defined('MOODLE_INTERNAL') || die();
use context_module;
use mod_certificate_external;
/**
* Mobile output class for certificate
*
* @package mod_certificate
* @copyright 2018 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mobile {
/**
* Returns the certificate course view for the mobile app.
* @param array $args Arguments from tool_mobile_get_content WS
*
* @return array HTML, javascript and otherdata
*/
public static function mobile_course_view($args) {
global $OUTPUT, $USER, $DB;
$args = (object) $args;
$cm = get_coursemodule_from_id('certificate', $args->cmid);
// Capabilities check.
require_login($args->courseid , false , $cm, true, true);
$context = context_module::instance($cm->id);
require_capability ('mod/certificate:view', $context);
if ($args->userid != $USER->id) {
require_capability('mod/certificate:manage', $context);
}
$certificate = $DB->get_record('certificate', array('id' => $cm->instance));
// Get certificates from external (taking care of exceptions).
try {
$issued = mod_certificate_external::issue_certificate($cm->instance);
$certificates = mod_certificate_external::get_issued_certificates($cm->instance);
$issues = array_values($certificates['issues']); // Make it mustache compatible.
} catch (Exception $e) {
$issues = array();
}
// Set timemodified for each certificate.
foreach ($issues as $issue) {
if (empty($issue->timemodified)) {
$issue->timemodified = $issue->timecreated;
}
}
$showget = true;
if ($certificate->requiredtime && !has_capability('mod/certificate:manage', $context)) {
if (certificate_get_course_time($certificate->course) < ($certificate->requiredtime * 60)) {
$showget = false;
}
}
$certificate->name = format_string($certificate->name);
list($certificate->intro, $certificate->introformat) =
external_format_text($certificate->intro, $certificate->introformat, $context->id,'mod_certificate', 'intro');
$data = array(
'certificate' => $certificate,
'showget' => $showget && count($issues) > 0,
'issues' => $issues,
'issue' => $issues[0],
'numissues' => count($issues),
'cmid' => $cm->id,
'courseid' => $args->courseid
);
return array(
'templates' => array(
array(
'id' => 'main',
'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
),
),
'javascript' => ,
'otherdata' => ,
'files' => $issues
);
}
}
Let’s go through the function code to analyse the different parts.
- Function declaration
- 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…)
- Function implementation
- 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.
Finally, we return:
- 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.
- JavaScript: Empty, because we don’t need any in this case
- 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.
- Files: A list of files that the app should be able to download (for offline usage mostly)
Step 3. Creating the template for the main function
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.
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/
All the HTML attributes starting with core- are custom components of the Mobile app.
File contents: mod/certificate/templates/mobile_view_page.mustache
{{=<% %>=}}
<core-course-module-description description="<% certificate.intro %>" component="mod_certificate" componentId="<% cmid %>"></core-course-module-description>
<ion-list>
<ion-list-header>
Template:'plugin.mod certificate.summaryofattempts'
</ion-list-header>
<%#issues%>
<ion-item>
<button ion-button block color="light" core-site-plugins-new-content title="<% certificate.name %>" component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
Template:'plugin.mod certificate.viewcertificateviews'
</button>
</ion-item>
<%/issues%>
<%#showget%>
<ion-item>
<button ion-button block core-course-download-module-main-file moduleId="<% cmid %>" courseId="<% certificate.course %>" component="mod_certificate" [files]="[{fileurl: '<% issue.fileurl %>', filename: '<% issue.filename %>', timemodified: '<% issue.timemodified %>', mimetype: '<% issue.mimetype %>'}]">
<ion-icon name="cloud-download" item-start></ion-icon>
Template:'plugin.mod certificate.getcertificate'
</button>
</ion-item>
<%/showget%>
<%^showget%>
<ion-item>
Template:'plugin.mod certificate.requiredtimenotmet'
</ion-item>
<%/showget%>
<span core-site-plugins-call-ws-on-load name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}">
</ion-list>
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).
Then we display the module description using <core-course-module-description that is a component used to include the course module description.
For displaying the certificate information we create a list of elements, adding a header on top.
The following line Template:'plugin.mod certificate.summaryofattempts' indicates that the Mobile app will translate the summaryofattempts 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:
“plugin” + plugin identifier (from mobile.php) + string id (the string must be indicated in the lang field in mobile.php).
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) core-site-plugins-new-content 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).
Just after this button we display another one but this time for downloading an issued certificate. The core-course-download-module-main-file 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.
Finally, just before the ion-list is closed, we use the core-site-plugins-call-ws-on-load 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 mod_certificate_view_certificate that will log that the user viewed this page.
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.