Note:

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

Moodle Mobile 2 (Ionic 1) Developing a plugin tutorial: Difference between revisions

From MoodleDocs
No edit summary
No edit summary
 
(22 intermediate revisions by 5 users not shown)
Line 1: Line 1:
{{Moodle Mobile}}
{{Template:WillNotMigrate}}
{{Moodle Mobile 2 (Ionic 1)}}
 
IMPORTANT: This documentation will be obsolete in May 2018, please [https://moodle.org/mod/forum/discuss.php?d=360412 read this announcement].


== Introduction ==
== Introduction ==


This tutorial describes all the steps needed for developing a plugin for the MoodleMobile app.
This tutorial describes all the steps needed for developing an add-on for the Moodle Mobile app.


In this tutorial we make the following assumptions:
In this tutorial we make the following assumptions:


* You have a medium/good skills/understand of Javascript, PHP and Moodle architecture
* You have a medium/good skills/understand of AngularJS/Ionic, PHP and Moodle architecture
* You know what a Web Service is and how it works in Moodle
* You know what a Web Service is and how it works in Moodle
* The plugin you are going to develop requires a Web Service in your Moodle installation to work
* The plugin you are going to develop requires a Web Service in your Moodle installation to work


'''Notice that neither the moodle plugin code and the moodle mobile plugin code are suitable for production environments'''
There is a second part of this tutorial where we extend the add-on features [[Moodle Mobile Developing a plugin tutorial part 2]]
 
There is a second part of this tutorial where we extend the current WebServices and create a new "forums" plugin, see [[Moodle Mobile Developing a plugin tutorial part 2]]
 
== Plugin spec ==
 
We want to develop a "grades" plugin that will display the current user grades for all the gradable activities in a course the user is enroll in.


A screenshot of the desired result:
== Add-on spec ==


[[File:MoodleMobileTutorial01.png]]
We want to develop a plugin that will display a new button in the user profile for adding new notes to the user. This plugin is suitable for course managers of teachers.


As you can see there is a new menu called "Grades" that displays a page in the middle panel showing all the course activities. When we click in an activity, the right panel shows the grade / maximum grade for the current user.
A couple of screenshots with the desired result:


In order to achieve this we will have to develop:
{|
* A plugin for Moodle containing a new Web Service that returns grades and..
| [[File:MoodleMobile2Tutorial01.png|thumb|Initial  view]]
* A MoodleMobile plugin
| [[File:MoodleMobile2Tutorial02.png|thumb|Course contents view]]
|}


== Steps ==
== Steps ==
Line 33: Line 31:
=== Set up your development environment ===
=== Set up your development environment ===


Clone or fork the official git repository [https://github.com/moodlehq/moodlemobile] in a local directory.
Please, read [[Setting up your development environment for Moodle Mobile 2]]
 
Set up your development browser, see [[Moodle_Mobile]], I mainly use the Google Chrome browser.


=== Set up your Moodle installation ===
=== Set up your Moodle installation ===
Line 41: Line 37:
Enable debugging, disable caches, etc... the typical settings for developing.
Enable debugging, disable caches, etc... the typical settings for developing.


Enable the Web Services and also the protocols you are going to use for testing and developing (REST is mandatory). See [[Web Services]] documentation.
Enable the Mobile Service via Admin / Plugins / Web Services / Mobile
 
=== Develop the Moodle local/mod/block/... plugin ===
 
You can find the full source code here:
https://github.com/cvaconsulting/moodle-local_custommm
 
In this case we've developed a local plugin that contains a Web Service that returns grades as described here: https://tracker.moodle.org/browse/MDL-30085
 
Notice that my version has some differences with the previous mentioned one. I have fixed some small bugs and changed some parameters.
 
=== Test your Web Services using a demo client ===
 
Before continuing you must test that your Web Service works, the local plugin contains a demo REST client, see:
 
https://github.com/cvaconsulting/moodle-local_custommm/blob/master/client/client.php
 
=== Create your custom Service ===
 
A service is a set of functions for an specific purpose. Since Moodle 2.1 there is a pre-built service that contains all the functions needed for the official mobile app.
 
This service cannot be edited for adding new functions so we don't have a simple way for make available our new Web Service function to the mobile app.


The only way for fix this is creating a new Service in Moodle, and configuring our app for pointing to this new service.
=== Develop the Moodle Web Services you are going to need ===


There are two ways of creating a Service in Moodle:
In this case we'll use an existing web service (core_notes_create_notes) that is already available in the Moodle Mobile app service, if you need custom functions you will need to develop a local plugin including there your new Web Services (see https://moodle.org/plugins/view/local_wstemplate) and create a custom service.


* Using a db/services.php in your plugin as mentioned here: [[External services description]]
=== Develop the Moodle Mobile add-on ===


* Creating the service in your Moodle installation using the forms in Admin > Plugins > Web Services > External Services
The full source code can be found here: https://github.com/moodlehq/moodlemobile2/tree/master/www/addons/notes


In this case we've created the service using the Moodle forms because it gives more flexibility in some aspects, there is a big impediment, due to https://tracker.moodle.org/browse/MDL-29807 in order to add a shortname for the new Service, we have to edit manually the Moodle database for adding the shortname in the dbprefix_external_services table
'''www/addons/notes/main.js'''
<syntaxhighlight lang="javascript">
angular.module('mm.addons.notes', [])


[[File:MoodleMobileTutorial02.png]]
.run(function($mmUserDelegate, $mmaNotesHandlers) {


When creating, the flats "Can download files" and "Enabled" must be checked.
    $mmUserDelegate.registerPlugin('mmaNotes:addNote', $mmaNotesHandlers.addNote);


=== Set up your MoodleMobile client ===
});
</syntaxhighlight>


The most important change you have to do is edit the config.json file and change the wsservice parameter in order to point to the shortname of the Service you created before.
Here we declare the plugin, and register the Plugin (this means that the plugin will be displayed in the User profile.


You have to add also the name of the plugin you are developing at the final of the plugins parameter:
We will use the $mmaNotesHandlers service for holding all the code related to the plugin availability and main functions


"plugins" : ["notifications", "upload", "contents", "participants", "addcontact", "addnote", "sendmessage", "grades"],
There are more delegates for registering your plugin, see:
* www/addons/mod_forum/main.js (Module delegate, and module actions delegate)
* www/addons/messages/main.js (Main side menu delegate, and multiple delegates for the user profile)


Notice that, for avoid errors, this last change should be done once the file plugins/grades/main.js file exists
'''www/addons/notes/services/handlers.js'''
<syntaxhighlight lang="javascript">


=== Develop the MoodleMobile plugin ===
self.isEnabled = function() {
    return $mmaNotes.isPluginEnabled();
};


The full source code can be found here:
self.isEnabledForUser = function(user, courseId) {
  // Active course required.
  return courseId && user.id != $mmSite.getUserId();
};
</syntaxhighlight>


https://github.com/cvaconsulting/moodlemobile-grades
These are mandatory functions that will check our plugin availability, the first one will rely in the $mmaNotes service (isPluginEnabled function), the second ones checks that the user profile is being viewed inside a course (because the WS requires it) and that the current user is not the required user (so you don't see the buttons in your own profile).


https://github.com/cvaconsulting/moodlemobile-grades/blob/master/lang/en.json - Contains the language strings, the string "pluginPLUGINname" is mandatory
<syntaxhighlight lang="javascript">


https://github.com/cvaconsulting/moodlemobile-grades/blob/master/icon.png - The plugin icon
        self.getController = function(user, courseid) {


https://github.com/cvaconsulting/moodlemobile-grades/blob/master/activities.html - The template for the activities list (center panel)
            return function($scope) {


https://github.com/cvaconsulting/moodlemobile-grades/blob/master/activitygrade.html - The template for showing the grade for an activity (right panel)
                // Button title.
                $scope.title = 'mma.notes.addnewnote';


https://github.com/cvaconsulting/moodlemobile-grades/blob/master/main.js - The plugin's main code
                $ionicModal.fromTemplateUrl('addons/notes/templates/add.html', {
                    scope: $scope,
                    animation: 'slide-in-up'
                }).then(function(m) {
                    $scope.modal = m;
                });


Bellow, I summarize the most important parts of the code (always parts of the main.js file):
                $scope.closeModal = function(){
                    $scope.modal.hide();
                };


<code javascript>
                $scope.addNote = function(){
                    // Freeze the add note button.
                    $scope.processing = true;


var templates = [
                    $mmaNotes.addNote(user.id, courseid, $scope.note.publishstate, $scope.note.text).then(function() {
    "root/externallib/text!root/plugins/grades/activities.html",
                        $translate('mma.notes.eventnotecreated').then(function(str) {
    "root/externallib/text!root/plugins/grades/activitygrade.html",
                            $ionicLoading.show({
    "root/externallib/text!root/plugins/grades/lang/en.json"
                                template: str,
];
                                duration: 2000
                            });
                        });
                    }, function(error) {
                        $mmUtil.showErrorModal(error);
                    }).finally(function() {
                        $scope.closeModal();
                    });
                };


define(templates,function (activities, activityGrade, langStrings) {
                $scope.action = function($event) {
    var plugin = {
                    $event.preventDefault();
        settings: {
                    $event.stopPropagation();
            name: "grades",
            type: "course",
            menuURL: "#course/grades/",
            lang: {
                component: "local_custommm",
strings: langStrings
            }
        },


</code>
                    $scope.note = {
                        publishstate: 'personal',
                        text: ''
                    };
                    $scope.processing = false;


Notice that we required three files, the two templates (.html) and the .json file that contains the lang strings.
                    $scope.modal.show();
                };
            };
        };


In the plugin.settings structure, we define the lant attribute that contains two very important attributes:
</syntaxhighlight>


* Component: It must refer to the Moodle component linked to this plugin
These is the main function code, it does the following:
* strings: The lang strings in .json format (plain text). As you can see, with this line we are populating the plugin.settings.lang.strings var withe the contents of the lang/en.json files
* $scope.title = 'mma.notes.addnewnote'; This sets the button title, note that we reference to the addnewnote string that is in the www/addons/notes/lang/en.json file (we need to prefix the string id to identify the plugin)
* $ionicModal.fromTemplateUrl - Creates the modal we are going to use for displaying the form, the HTML code of the form is in www/addons/notes/templates/add.html
* $scope.closeModal - Helper function for closing the modal inside the modal via a button
* $scope.addNote - This function will be called when the user clicks the "Add a note" button in the modal, it will use the $mmaNotes service that handles all the logic WS logic for creating the note
* $scope.action - This function will be called when the user clicks the "Add a note" button in the user profile view, it will open the modal and set the default values


<code javascript>
'''www/addons/notes/services/notes.js'''
<syntaxhighlight lang="javascript">


        viewActivities: function(courseId) {
angular.module('mm.addons.notes')


            MM.panels.showLoading('center');
.factory('$mmaNotes', function($mmSite, $log, $q) {
    $log = $log.getInstance('$mmaNotes');


            if (MM.deviceType == "tablet") {
    var self = {};
                MM.panels.html('right', '');
            }


            var data = {
    self.addNote = function(userId, courseId, publishState, noteText) {
             "options[0][name]" : "",
        var data = {
             "options[0][value]" : ""
             "notes[0][userid]" : userId,
             };          
            "notes[0][publishstate]": publishState,
            data.courseid = courseId;
             "notes[0][courseid]": courseId,
           
            "notes[0][text]": noteText,
            MM.moodleWSCall('core_course_get_contents', data, function(contents) {
             "notes[0][format]": 1
                var course = MM.db.get("courses", MM.config.current_site.id + "-" + courseId);
        };
        return $mmSite.write('core_notes_create_notes', data);
    };


                var tpl = {
    self.isPluginEnabled = function() {
                    sections: contents,
        var infos;
                    course: course.toJSON() // Convert a model to a plain javascript object.
                }
                var html = MM.tpl.render(MM.plugins.grades.templates.activities.html, tpl);
                MM.panels.show("center", html);
            });
        },


</code>
        if (!$mmSite.isLoggedIn()) {
            return false;
        } else if (!$mmSite.canUseAdvancedFeature('enablenotes')) {
            return false;
        } else if (!$mmSite.wsAvailable('core_notes_create_notes')) {
            return false;
        }


The viewActivities is the function that displays in the center panel the list of activities inside a course, as you can see the code is pretty simple.
        return true;
* We call to the core_course_get_contents Web Service that returns all the activities and resources inside a course
    };
* We render the template that will iterate over the sections and activities displaying the links to specifics activities


<code javascript>
    return self;
        viewActivityGrade: function(activityName, courseId, modName, cmId) {
});
</syntaxhighlight>


This file contains the notes service:
* isPluginEnabled - Checks if the notes functionality is available by checking Moodle settings and the required WS availability
* addNote - Will call the remote WS for adding the note via the $mmSite.write helper method (the $mmService has also a read method for retrieving data)


             if (MM.deviceType == "tablet") {
'''/www/addons/notes/templates/add.html'''
                 MM.panels.showLoading('right');
<syntaxhighlight lang="html4strict">
             }
<ion-modal-view class="product edit create">
    <form name="itemEdit" ng-submit="addNote()">
        <ion-header-bar class="bar-header">
            <h1 class="title">{{ 'mma.notes.addnewnote' | translate }}</h1>
             <a class="button" ng-click="closeModal()">{{ 'mm.core.cancel' | translate }}</a>
        </ion-header-bar>
        <ion-content has-bouncing="true">
            <div class="row">
                <div class="col">
                    <label class="item item-input item-select">
                        <div class="input-label">
                          {{ 'mma.notes.publishstate' | translate }}
                        </div>
                        <select ng-model="note.publishstate" name="publishstate">
                            <option value="personal">{{ 'mma.notes.personalnotes' | translate }}</option>
                            <option value="course">{{ 'mma.notes.coursenotes' | translate }}</option>
                            <option value="site">{{ 'mma.notes.sitenotes' | translate }}</option>
                        </select>
                    </label>
                 </div>
            </div>
            <div class="row">
                <div class="col">
                    <label class="item item-input text">
                        <textarea placeholder="{{ 'mma.notes.note' | translate }}" rows="5" ng-model="note.text" name="text" lose-focus-on-return required="required"></textarea>
                    </label>
                </div>
            </div>
            <div class="padding">
                <button type="submit" class="button button-block" ng-disabled="processing || !note.text.length || note.text.length < 2">
                    {{ 'mma.notes.addnewnote' | translate }}
                </button>
             </div>
        </ion-content>
    </form>
</ion-modal-view>
</syntaxhighlight>


            var data = {
This file includes the code for the modal. As you can see the close button will call the closeModal() function meanwhile the Add a note button will submit the form that will invoke the addNote function
                "grades[courseid]" : courseId,
                "grades[component]" : "mod_" + modName,
                "grades[cmid]" : cmId,
                "grades[userids][0]" : MM.config.current_site.userid
            };           
           
            MM.moodleWSCall('local_custommm_get_grades', data, function(contents) {
                var course = MM.db.get("courses", MM.config.current_site.id + "-" + courseId);
               
                var grade= MM.lang.s("nogrades", "grades");
               
                if(contents.items[0]) {
                    grade = contents.items[0]["grades"][0]["str_long_grade"];
                }


                var tpl = {
For more information, refer to http://ionicframework.com/docs/api/service/$ionicModal/
                    activityName: activityName,
                    sections: contents,
                    finalGrade: grade,
                    course: course.toJSON(), // Convert a model to a plain javascript object.
                    items: contents
                }
                var html = MM.tpl.render(MM.plugins.grades.templates.activityGrade.html, tpl);
                MM.panels.show("right", html);
                if (MM.deviceType == "tablet" && contents.length > 0) {
                    // First section.
                    MM.plugins.contents.viewCourseContentsSection(courseId, 0);
                }
            });
        },
</code>
 
This function gets and displays the grade for the selected activity in the centered panel.
 
We just call to the Web Service we developed and parse the result, if nothing is returned we display a generic message indicating that there is no grades information for the activity.
If a grades full structure is returned, we return the first grade for the first grade items (as Moodle usually does) that corresponds to the user grade for the activity
 
Notice that:
* Some activities, like workshop, returns two different grades items (teacher and peer grades). The example doesn't implement any logic for this case.
* Some activities may return outcomes. The example doesn't implement any logic for this case.
* The rest grade information (grade min, grade max, feedback) is also covered by this example.
 
=== Deliver your plugin ===
 
Android: Package your plugin using [https://build.phonegap.com/apps PhoneGap build]
 
iOs: Further instructions soon


== See also ==
== See also ==


[[Moodle Mobile Developing a plugin tutorial part 2]]
[[Moodle Mobile Developing a plugin tutorial part 2]]
[[Category: Tutorial]]

Latest revision as of 07:14, 29 April 2022


Warning: This page is no longer in use. The information contained on the page should NOT be seen as relevant or reliable.



Warning: This page is no longer in use. The information contained on the page should NOT be seen as relevant or reliable.


IMPORTANT: This documentation will be obsolete in May 2018, please read this announcement.

Introduction

This tutorial describes all the steps needed for developing an add-on for the Moodle Mobile app.

In this tutorial we make the following assumptions:

  • You have a medium/good skills/understand of AngularJS/Ionic, PHP and Moodle architecture
  • You know what a Web Service is and how it works in Moodle
  • The plugin you are going to develop requires a Web Service in your Moodle installation to work

There is a second part of this tutorial where we extend the add-on features Moodle Mobile Developing a plugin tutorial part 2

Add-on spec

We want to develop a plugin that will display a new button in the user profile for adding new notes to the user. This plugin is suitable for course managers of teachers.

A couple of screenshots with the desired result:

Initial view
Course contents view

Steps

Set up your development environment

Please, read Setting up your development environment for Moodle Mobile 2

Set up your Moodle installation

Enable debugging, disable caches, etc... the typical settings for developing.

Enable the Mobile Service via Admin / Plugins / Web Services / Mobile

Develop the Moodle Web Services you are going to need

In this case we'll use an existing web service (core_notes_create_notes) that is already available in the Moodle Mobile app service, if you need custom functions you will need to develop a local plugin including there your new Web Services (see https://moodle.org/plugins/view/local_wstemplate) and create a custom service.

Develop the Moodle Mobile add-on

The full source code can be found here: https://github.com/moodlehq/moodlemobile2/tree/master/www/addons/notes

www/addons/notes/main.js

angular.module('mm.addons.notes', [])

.run(function($mmUserDelegate, $mmaNotesHandlers) {

    $mmUserDelegate.registerPlugin('mmaNotes:addNote', $mmaNotesHandlers.addNote);

});

Here we declare the plugin, and register the Plugin (this means that the plugin will be displayed in the User profile.

We will use the $mmaNotesHandlers service for holding all the code related to the plugin availability and main functions

There are more delegates for registering your plugin, see:

  • www/addons/mod_forum/main.js (Module delegate, and module actions delegate)
  • www/addons/messages/main.js (Main side menu delegate, and multiple delegates for the user profile)

www/addons/notes/services/handlers.js

self.isEnabled = function() {
    return $mmaNotes.isPluginEnabled();
};

self.isEnabledForUser = function(user, courseId) {
   // Active course required.
   return courseId && user.id != $mmSite.getUserId();
};

These are mandatory functions that will check our plugin availability, the first one will rely in the $mmaNotes service (isPluginEnabled function), the second ones checks that the user profile is being viewed inside a course (because the WS requires it) and that the current user is not the required user (so you don't see the buttons in your own profile).

        self.getController = function(user, courseid) {

            return function($scope) {

                // Button title.
                $scope.title = 'mma.notes.addnewnote';

                $ionicModal.fromTemplateUrl('addons/notes/templates/add.html', {
                    scope: $scope,
                    animation: 'slide-in-up'
                }).then(function(m) {
                    $scope.modal = m;
                });

                $scope.closeModal = function(){
                    $scope.modal.hide();
                };

                $scope.addNote = function(){
                    // Freeze the add note button.
                    $scope.processing = true;

                    $mmaNotes.addNote(user.id, courseid, $scope.note.publishstate, $scope.note.text).then(function() {
                        $translate('mma.notes.eventnotecreated').then(function(str) {
                            $ionicLoading.show({
                                template: str,
                                duration: 2000
                            });
                        });
                    }, function(error) {
                        $mmUtil.showErrorModal(error);
                    }).finally(function() {
                        $scope.closeModal();
                    });
                };

                $scope.action = function($event) {
                    $event.preventDefault();
                    $event.stopPropagation();

                    $scope.note = {
                        publishstate: 'personal',
                        text: ''
                    };
                    $scope.processing = false;

                    $scope.modal.show();
                };
            };
        };

These is the main function code, it does the following:

  • $scope.title = 'mma.notes.addnewnote'; This sets the button title, note that we reference to the addnewnote string that is in the www/addons/notes/lang/en.json file (we need to prefix the string id to identify the plugin)
  • $ionicModal.fromTemplateUrl - Creates the modal we are going to use for displaying the form, the HTML code of the form is in www/addons/notes/templates/add.html
  • $scope.closeModal - Helper function for closing the modal inside the modal via a button
  • $scope.addNote - This function will be called when the user clicks the "Add a note" button in the modal, it will use the $mmaNotes service that handles all the logic WS logic for creating the note
  • $scope.action - This function will be called when the user clicks the "Add a note" button in the user profile view, it will open the modal and set the default values

www/addons/notes/services/notes.js

angular.module('mm.addons.notes')

.factory('$mmaNotes', function($mmSite, $log, $q) {
    $log = $log.getInstance('$mmaNotes');

    var self = {};

    self.addNote = function(userId, courseId, publishState, noteText) {
        var data = {
            "notes[0][userid]" : userId,
            "notes[0][publishstate]": publishState,
            "notes[0][courseid]": courseId,
            "notes[0][text]": noteText,
            "notes[0][format]": 1
        };
        return $mmSite.write('core_notes_create_notes', data);
    };

    self.isPluginEnabled = function() {
        var infos;

        if (!$mmSite.isLoggedIn()) {
            return false;
        } else if (!$mmSite.canUseAdvancedFeature('enablenotes')) {
            return false;
        } else if (!$mmSite.wsAvailable('core_notes_create_notes')) {
            return false;
        }

        return true;
    };

    return self;
});

This file contains the notes service:

  • isPluginEnabled - Checks if the notes functionality is available by checking Moodle settings and the required WS availability
  • addNote - Will call the remote WS for adding the note via the $mmSite.write helper method (the $mmService has also a read method for retrieving data)

/www/addons/notes/templates/add.html

<ion-modal-view class="product edit create">
    <form name="itemEdit" ng-submit="addNote()">
        <ion-header-bar class="bar-header">
            <h1 class="title">{{ 'mma.notes.addnewnote' | translate }}</h1>
            <a class="button" ng-click="closeModal()">{{ 'mm.core.cancel' | translate }}</a>
        </ion-header-bar>
        <ion-content has-bouncing="true">
            <div class="row">
                <div class="col">
                    <label class="item item-input item-select">
                        <div class="input-label">
                          {{ 'mma.notes.publishstate' | translate }}
                        </div>
                        <select ng-model="note.publishstate" name="publishstate">
                            <option value="personal">{{ 'mma.notes.personalnotes' | translate }}</option>
                            <option value="course">{{ 'mma.notes.coursenotes' | translate }}</option>
                            <option value="site">{{ 'mma.notes.sitenotes' | translate }}</option>
                        </select>
                    </label>
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <label class="item item-input text">
                        <textarea placeholder="{{ 'mma.notes.note' | translate }}" rows="5" ng-model="note.text" name="text" lose-focus-on-return required="required"></textarea>
                    </label>
                </div>
            </div>
            <div class="padding">
                <button type="submit" class="button button-block" ng-disabled="processing || !note.text.length || note.text.length < 2">
                    {{ 'mma.notes.addnewnote' | translate }}
                </button>
            </div>
        </ion-content>
    </form>
</ion-modal-view>

This file includes the code for the modal. As you can see the close button will call the closeModal() function meanwhile the Add a note button will submit the form that will invoke the addNote function

For more information, refer to http://ionicframework.com/docs/api/service/$ionicModal/

See also

Moodle Mobile Developing a plugin tutorial part 2