tag:blogger.com,1999:blog-70949868554572899582024-03-17T23:04:00.019-04:00Technology and LearningThoughts on learning and education and how technology is changing the way we perceive and perform them.Mike Churchwardhttp://www.blogger.com/profile/07504877392767978258noreply@blogger.comBlogger51125tag:blogger.com,1999:blog-7094986855457289958.post-55040299937522257162018-06-18T14:57:00.000-04:002018-06-18T14:57:02.422-04:00Implementing Moodle's Privacy API in a Moodle Plugin - Part 4Continuing the series on implementing Moodle's Privacy API in my
questionnaire plugin, I will add code to handle all of the questionnaire data. I did my smaller test set, but now I need to get a fully working implementation.<br />
<br />
To start with, I need to add all of the potential response data to my privacy data provider. For questionnaire, this includes multiple question and response tables. For the export, I also need to design an appropriate output structure. This post will work on fulfilling these functions.<br />
<br />
I have a couple of options for an output structure. I can create one JSON structure with all of the responses, questions and answers, like this:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;">{
<span style="color: #660e7a; font-weight: bold;">"name"</span>: <span style="color: green; font-weight: bold;">"Test Questionnaire"</span>,
<span style="color: #660e7a; font-weight: bold;">"intro"</span>: <span style="color: green; font-weight: bold;">"</span><span style="color: green; font-weight: bold;">A wonderful description of the questionnaire.</span><span style="color: green; font-weight: bold;">"</span>,
<span style="color: #660e7a; font-weight: bold;">"responses"</span>: [
{
<span style="color: #660e7a; font-weight: bold;">"complete"</span>: <span style="color: green; font-weight: bold;">"Yes"</span>,
<span style="color: #660e7a; font-weight: bold;">"lastsaved"</span>: <span style="color: green; font-weight: bold;">"Friday, 18 November 2016, 8:14 pm"</span>,
<span style="color: #660e7a; font-weight: bold;">"questions" </span>: [
{
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>name"</span>: <span style="color: green; font-weight: bold;">"Q1. Car ownership"</span>,
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>text"</span>: <span style="color: green; font-weight: bold;">"Do you own a car?"</span>,
<span style="color: #660e7a; font-weight: bold;">"answers"</span>: [
<span style="color: green; font-weight: bold;">"No"</span><span style="color: green; font-weight: bold;">
</span>]
},
{
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>name"</span>: <span style="color: green; font-weight: bold;">"Q2. Characters"</span>,
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>text"</span>: <span style="color: green; font-weight: bold;">"Enter no more than 10 characters."</span>,
<span style="color: #660e7a; font-weight: bold;">"answers"</span>: [
<span style="color: green; font-weight: bold;">"123456"</span><span style="color: green; font-weight: bold;">
</span>]
},
{
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>name"</span>: <span style="color: green; font-weight: bold;">"Q3. Numbers"</span>,
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>text"</span>: <span style="color: green; font-weight: bold;">"Check all that apply"</span>,
<span style="color: #660e7a; font-weight: bold;">"answers"</span>: [
<span style="color: green; font-weight: bold;">"1,3,5,Another number: 7"</span><span style="color: green; font-weight: bold;">
</span>]
},
{
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>name"</span>: <span style="color: green; font-weight: bold;">"Q4. Rate course"</span>,
<span style="color: #660e7a; font-weight: bold;">"</span><span style="color: #660e7a; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">question</span>text"</span>: <span style="color: green; font-weight: bold;">"Rate these"</span>,
<span style="color: #660e7a; font-weight: bold;">"answers"</span>: [
<span style="color: green; font-weight: bold;">"Formatting your course: Very easy to use"</span>,
<span style="color: green; font-weight: bold;">"Laying out your course: Easy to use"</span><span style="color: green; font-weight: bold;">
</span>]
}
]
}
]
}</pre>
</blockquote>
<br />
Or I could use <i>subcontexts</i>, and create a directory structure. Something like this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4rpQjicI8r6Q8YkmYPS73HjpvYWP5Gj8LC6SlxqSpXK6LGcWQaJlay7fnix_eIWhkdDnSBkq8-egWEASUOiB7zN-5MrgpD37j4otSyMAKSBTgFU39zvhY-Cs7OhdsVqWB8d8SvAZSQROu/s1600/Screen+Shot+2018-06-06+at+11.42.31+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="232" data-original-width="310" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4rpQjicI8r6Q8YkmYPS73HjpvYWP5Gj8LC6SlxqSpXK6LGcWQaJlay7fnix_eIWhkdDnSBkq8-egWEASUOiB7zN-5MrgpD37j4otSyMAKSBTgFU39zvhY-Cs7OhdsVqWB8d8SvAZSQROu/s1600/Screen+Shot+2018-06-06+at+11.42.31+AM.png" /></a></div>
<br />
In this case, each response by the user would have its own directory, with a separate subdirectory for each question and its specific response. This would be done using subcontexts. I feel the first option is really the best choice for what I need. The second one seems like overkill.<br />
<br />
Building the directory structure is done in the forum module. I'm not completely sure how it works, but if you walk through the <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/mod/forum/classes/privacy/provider.php" target="_blank">forum's provider file</a>, you can see that it is built through arrays, and exported through nested calls to <span style="font-family: "courier new" , "courier" , monospace;">export_data</span>.<br />
<br />
In any case, building a JSON structure like the one I planned above, is not too difficult. I already have a function in questionnaire that returns a structured data set that I use when sending responses via email or other notification methods. It isn't exactly what I need, but is close enough. So I'll modify it and make sure it still works for the code that uses it currently. The modified code is <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/M35_PRIVACY_API/questionnaire.class.php#L1779" target="_blank">here</a>, and I create another <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/M35_PRIVACY_API/questionnaire.class.php#L1767" target="_blank">quick function</a> to use in the privacy provider. The rewrite of my export function that provides all appropriate response data now looks like <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/M35_PRIVACY_API/classes/privacy/provider.php#L142" target="_blank">this</a>.<br />
<br />
The last thing I require is the full deletion functions. I do have some library functions that delete response data, but they also log events. The core plugins all seem to provide direct database deletions rather than using their deletion library functions. So, I'll do the same, creating one function that does most of the deletion work so that the two privacy API functions don't have to duplicate that code. The end result is <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/M35_PRIVACY_API/classes/privacy/provider.php#L214" target="_blank">here</a>.<br />
<br />
I have tested all of this code with the <a href="https://docs.moodle.org/dev/Privacy_API/Utilities" target="_blank">test code Moodle provided</a>. It all seemed to work fine. I'll see if I can create some automated tests into the module's testing code as well, and do more testing before I release this.<br />
<br />
If you have any questions about this work, please ask here or in the forums on Moodle.org.<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Mike Churchwardhttp://www.blogger.com/profile/07504877392767978258noreply@blogger.com1tag:blogger.com,1999:blog-7094986855457289958.post-50729773634221671182018-06-06T10:31:00.000-04:002018-06-06T10:31:07.946-04:00 Implementing Moodle's Privacy API in a Moodle Plugin - Part 3Continuing the series on implementing Moodle's Privacy API in my questionnaire plugin, I will add code to handle deletion of user data.<br />
<br />
The <a href="https://docs.moodle.org/dev/Privacy_API#Providing_a_way_to_delete_user_data" target="_blank">documentation</a> indicates that there are two functions to implement. The <span style="font-family: "courier new" , "courier" , monospace;">delete_data_for_all_users_in_context</span> handles deleting all users' data for a provided context when a defined retention period has expired. The retention period is part of the new privacy settings. The <span style="font-family: "courier new" , "courier" , monospace;">delete_data_for_user</span> handles deleting user data for the provided contexts, when a user has requested to be forgotten.<br />
<br />
Looking at the examples in the documentation and in the two modules I have been referring to, <i>forum</i> and <i>choice</i>, these functions determine the data records that need to be deleted and then delete them from the database. Doing it this way, instead of using a specific module's API, seems odd to me. I would have thought using the module API would be safer. But it also means that the data is deleted without leaving information about why and how it was deleted. Most API's would log a deleting event in order to have accountability for the activity. It's possible that logging this deletion violates the GDPR's "forget me" policy? I will need to look into this.<br />
<br />
For now, I'll follow the same strategy, and create the record deletion code in these functions.<br />
<br />
Continuing with my simplified example, using only the attempts table, these functions are very straightforward. The <span style="font-family: "courier new" , "courier" , monospace;">delete_data_for_all_users_in_context</span> function needs to delete all of the questionnaire_attempts records with the questionnaire id of the context passed into the function. So, the <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/24954343b138d93b29cdf37d918943d9ec24ac34/classes/privacy/provider.php#L156" target="_blank">code looks like this</a>:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: navy; font-weight: bold;">public static function </span><span style="background-color: #f7faff;">delete_data_for_all_users_in_context(\context </span><span style="background-color: #f7faff; color: #660000;">$context</span><span style="background-color: #f7faff;">) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">global </span><span style="background-color: #f7faff; color: #660000;">$DB</span><span style="background-color: #f7faff;">;</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">if </span><span style="background-color: #f7faff;">(!(</span><span style="background-color: #f7faff; color: #660000;">$context </span><span style="background-color: #f7faff; color: navy; font-weight: bold;">instanceof </span><span style="background-color: #f7faff;">\context_module)) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">return</span><span style="background-color: #f7faff;">;</span><span style="background-color: #f7faff;">
}</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">if </span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$cm </span><span style="background-color: #f7faff;">= get_coursemodule_from_id(</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'questionnaire'</span><span style="background-color: #f7faff;">, </span><span style="background-color: #f7faff; color: #660000;">$context</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: navy; font-weight: bold;">instanceid</span><span style="background-color: #f7faff;">)) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: #660000;">$DB</span><span style="background-color: #f7faff;">->delete_records(</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'questionnaire_attempts'</span><span style="background-color: #f7faff;">, [</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'qid' </span><span style="background-color: #f7faff;">=> </span><span style="background-color: #f7faff; color: #660000;">$cm</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">instance</span><span style="background-color: #f7faff;">]);</span><span style="background-color: #f7faff;">
}</span>
<span style="background-color: #f7faff;">}</span></pre>
</blockquote>
The <span style="font-family: "courier new" , "courier" , monospace;">delete_data_for_user</span> function needs to delete all data for each provided context for the specified user. The parameter passed in is a new structure, <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/approved_contextlist.php" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">\core_privacy\local\request\approved_contextlist</span></a>, which contains the user and the context information we need. It provides methods to get the user and context information. Knowing that, the code becomes very similar to the previous function, except that it will delete all of the attempt records with the contexts' questionnaire id's and the specified user id. The <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/24954343b138d93b29cdf37d918943d9ec24ac34/classes/privacy/provider.php#L173" target="_blank">code looks like this</a>:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: navy; font-weight: bold;">public static function </span><span style="background-color: #f7faff;">delete_data_for_user(\core_privacy\local\request\approved_contextlist </span><span style="background-color: #f7faff; color: #660000;">$contextlist</span><span style="background-color: #f7faff;">) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">global </span><span style="background-color: #f7faff; color: #660000;">$DB</span><span style="background-color: #f7faff;">;</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">if </span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">empty</span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$contextlist</span><span style="background-color: #f7faff;">->count())) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">return</span><span style="background-color: #f7faff;">;</span><span style="background-color: #f7faff;">
}</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: #660000;">$userid </span><span style="background-color: #f7faff;">= </span><span style="background-color: #f7faff; color: #660000;">$contextlist</span><span style="background-color: #f7faff;">->get_user()-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">id</span><span style="background-color: #f7faff;">;</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">foreach </span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$contextlist</span><span style="background-color: #f7faff;">->get_contexts() </span><span style="background-color: #f7faff; color: navy; font-weight: bold;">as </span><span style="background-color: #f7faff; color: #660000;">$context</span><span style="background-color: #f7faff;">) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">if </span><span style="background-color: #f7faff;">(!(</span><span style="background-color: #f7faff; color: #660000;">$context </span><span style="background-color: #f7faff; color: navy; font-weight: bold;">instanceof </span><span style="background-color: #f7faff;">\context_module)) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">continue</span><span style="background-color: #f7faff;">;</span><span style="background-color: #f7faff;">
}</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: navy; font-weight: bold;">if </span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$cm </span><span style="background-color: #f7faff;">= get_coursemodule_from_id(</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'questionnaire'</span><span style="background-color: #f7faff;">, </span><span style="background-color: #f7faff; color: #660000;">$context</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: navy; font-weight: bold;">instanceid</span><span style="background-color: #f7faff;">)) {</span><span style="background-color: #f7faff;">
</span><span style="background-color: #f7faff; color: #660000;">$DB</span><span style="background-color: #f7faff;">->delete_records(</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'questionnaire_attempts'</span><span style="background-color: #f7faff;">, [</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'qid' </span><span style="background-color: #f7faff;">=> </span><span style="background-color: #f7faff; color: #660000;">$cm</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">instance</span><span style="background-color: #f7faff;">, </span><span style="background-color: #f7faff; color: green; font-weight: bold;">'userid' </span><span style="background-color: #f7faff;">=> </span><span style="background-color: #f7faff; color: #660000;">$userid</span><span style="background-color: #f7faff;">]);</span><span style="background-color: #f7faff;">
}</span><span style="background-color: #f7faff;">
}</span><span style="background-color: #f7faff;">
}</span></pre>
</blockquote>
To test these functions, I get the script provided from the <a href="https://docs.moodle.org/dev/Privacy_API/Utilities" target="_blank">Privacy API Utilities</a>. Executing this function allows me to specify a username which will have its data removed. Before I execute this on my test site, I backup a copy of the database. My functions are not complete at the moment and will only delete the "attempts" record, leaving other data intact. If my functions work, I can restore the database afterward.<br />
<br />
Executing the test script, shows a lot of output. Searching through that output, I find:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">Processing mod_questionnaire (42/515) (Monday, 4 June 2018, 8:44 pm)</span></blockquote>
which is good. And, when I check the <span style="font-family: "georgia" , "times new roman" , serif;">questionnaire_attempts</span> data table, I see that the records for that user have indeed been deleted. Looks like this part of the API is working.<br />
<br />
Now that I have the basic version working, I'll go back and make sure I do the complete job.<br />
<br />
Looking ahead, I may need to learn about <i>subcontexts</i>, which are used in the forum provider. On the <a href="https://docs.moodle.org/dev/Privacy_API#Exporting_data" target="_blank">API documentation page</a>, you can see it referred to. I believe its the key concept in creating the directory like structure of an export, as shown in the image below:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIdEP5Dzp2wGm-cI2rIytMGnJE6RX5Q4_n8svXpJNecHXTP9RoJInpV8NLtJk46tgdqB9x93DIwSnjBtUIgphc0DSUL6y0OIM1p-B7hUSSbsIss2fEoWSkD-q0h1WQ_vkF_-_HE6qvNpAj/s1600/Screen+Shot+2018-06-06+at+10.24.37+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="380" data-original-width="338" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIdEP5Dzp2wGm-cI2rIytMGnJE6RX5Q4_n8svXpJNecHXTP9RoJInpV8NLtJk46tgdqB9x93DIwSnjBtUIgphc0DSUL6y0OIM1p-B7hUSSbsIss2fEoWSkD-q0h1WQ_vkF_-_HE6qvNpAj/s320/Screen+Shot+2018-06-06+at+10.24.37+AM.png" width="284" /></a></div>
<br />
<br />
Stay tuned for Part 4, where I will determine if this is needed, and figure out how to do it.<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Mike Churchwardhttp://www.blogger.com/profile/07504877392767978258noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-14441435777740536082018-06-04T10:30:00.000-04:002018-06-06T10:35:14.268-04:00Implementing Moodle's Privacy API in a Moodle Plugin - Part 2<br />
In <a href="http://tandl.churchward.ca/2018/05/implementing-moodles-privacy-api-in.html">part 1</a>, I began implementing Moodle's Privacy API in my questionnaire plugin, in order to meet the requirements of the GDPR. In this post, I will add the specific code to do this.<br />
<br />
I have a <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/27bdd6531126c8364a564dbaafa0f3aa14e67509/classes/privacy/provider.php" target="_blank">skeleton file in place</a>, that includes all of the class and function specifications that I need. Next, I need to <a href="https://docs.moodle.org/dev/Privacy_API#Describing_data_stored_in_database_tables" target="_blank">describe each data table that includes user data</a>. The questionnaire has several tables that do this, namely:<br />
<ul>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_attempts</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_response</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_response_bool</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_response_date</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_response_other</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_response_rank</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_response_text</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_resp_multiple</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">questionnaire_resp_single</span></li>
</ul>
It looks like I need to add each of these to the <span style="font-family: "courier new" , "courier" , monospace;">$collection</span> variable. And, each table and relevant field will require a language string, as shown in the <a href="https://docs.moodle.org/dev/Privacy_API#Example_4" target="_blank">documentation example</a>. To start with, I'll implement just the <span style="font-family: "courier new" , "courier" , monospace;">questionnaire_attempts</span> table.<br />
<br />
Adding
this table to the <span style="font-family: "courier new" , "courier" , monospace;">get_metadata</span> function means defining the relevant
fields. In this case, this table stores the user id, the question id,
the response id and the time stamp of when the latest submission for
this attempt occurred. Each of these fields can be considered private
data, although the question id points to the actual question which
really only provides context for a specific question response. I'll stay
on the side of providing too much information rather than too little
and include it. My function now <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/714ed22c86a3e89426a445f449baa92dd993c455/classes/privacy/provider.php#L43" target="_blank">looks like</a>:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">public static function get_metadata(collection $collection) : collection {<br /><br /> // Add all of the relevant tables and fields to the collection.<br /> $collection->add_database_table('questionnaire_attempts', [<br /> 'userid' => 'privacy:metadata:questionnaire_attempts:userid',<br /> 'rid' => 'privacy:metadata:questionnaire_attempts:rid',<br /> 'qid' => 'privacy:metadata:questionnaire_attempts:qid',<br /> 'timemodified' => 'privacy:metadata:questionnaire_attempts:timemodified',<br /> ], 'privacy:metadata:questionnaire_attempts');<br /><br /> return $collection;<br />}</span></blockquote>
And, I add each of the privacy strings to the <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/714ed22c86a3e89426a445f449baa92dd993c455/lang/en/questionnaire.php#L376" target="_blank">language file</a> as:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">$string['privacy:metadata:questionnaire_attempts'] = 'Details about each submission of a questionnaire by a user.';<br />$string['privacy:metadata:questionnaire_attempts:userid'] = 'The ID of the user for this attempt.';<br />$string['privacy:metadata:questionnaire_attempts:rid'] = 'The ID of the user\'s response record for this attempt.';<br />$string['privacy:metadata:questionnaire_attempts:qid'] = 'The ID of the questionnaire record for this attempt.';<br />$string['privacy:metadata:questionnaire_attempts:timemodified'] = 'The timestamp for the latest submission of this attempt.';</span> </blockquote>
Now that I have added the metadata, I should be able to see them at the "Plugin privacy registry" page of the site. Navigating to that page, and opening the section on questionnaire, I do indeed see the definitions I just added:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJlHDzvbsvWjMSegiTUKjZ2uFxHDmktwBoR9KFOUwHUcGgGEcc7WDIjuAiF7hGHbJmsYIL1q_6joVF11fvYTcShF30gpSblFjTGggUCc6TfjSBF942jNHzVKDI85_7d_hGX-uTliB0A0eg/s1600/Screen+Shot+2018-06-01+at+9.28.44+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="296" data-original-width="752" height="156" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJlHDzvbsvWjMSegiTUKjZ2uFxHDmktwBoR9KFOUwHUcGgGEcc7WDIjuAiF7hGHbJmsYIL1q_6joVF11fvYTcShF30gpSblFjTGggUCc6TfjSBF942jNHzVKDI85_7d_hGX-uTliB0A0eg/s400/Screen+Shot+2018-06-01+at+9.28.44+AM.png" width="400" /></a></div>
<br />
<br />
Next, I need to provide a way to <a href="https://docs.moodle.org/dev/Privacy_API#Retrieving_the_list_of_contexts" target="_blank">retrieve and return the list of contexts</a> for which my plugin stores user data. For my plugin, the only context is <span style="font-family: "courier new" , "courier" , monospace;">CONTEXT_MODULE</span>. And I can determine the context module id for each questionnaire a user has responded to by the <span style="font-family: "courier new" , "courier" , monospace;">qid</span> field in the <span style="font-family: "courier new" , "courier" , monospace;">questionnaire_attempts</span> table and joining tables back through the <span style="font-family: "courier new" , "courier" , monospace;">course_modules</span> table to the <span style="font-family: "courier new" , "courier" , monospace;">context</span> table using SQL. My <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/714ed22c86a3e89426a445f449baa92dd993c455/classes/privacy/provider.php#L63" target="_blank">function looks like this</a>:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {<br /> $contextlist = new \core_privacy\local\request\contextlist();<br /><br /> $sql = "SELECT c.id<br /> FROM {context} c<br /> INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel<br /> INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname<br /> INNER JOIN {questionnaire} q ON q.id = cm.instance<br /> LEFT JOIN {questionnaire_attempts} qa ON qa.qid = q.id<br /> WHERE qa.userid = :attemptuserid<br /> ";<br /><br /> $params = [<br /> 'modname' => 'questionnaire',<br /> 'contextlevel' => CONTEXT_MODULE,<br /> 'attemptuserid' => $userid,<br /> ];<br /><br /> $contextlist->add_from_sql($sql, $params);<br /><br /> return $contextlist;<br />}</span></blockquote>
Next, I need to provide a way to <a href="https://docs.moodle.org/dev/Privacy_API#Providing_a_way_to_export_user_data" target="_blank">export user data</a>. The documentation doesn't provide an example, but I can find examples in the core code.<br />
<br />
There are a number of data types that must be exported mentioned in the <a href="https://docs.moodle.org/dev/Privacy_API#Exporting_data" target="_blank">documentation</a>, but questionnaire only needs to worry about the "data" part. The documentation section also describes using the <span style="font-family: "courier new" , "courier" , monospace;">\core_privacy\local\request\content_writer</span> but the code examples in the documentation use <span style="font-family: "courier new" , "courier" , monospace;">\core_privacy\local\request\writer</span>. Looking at the <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/content_writer.php" target="_blank">/privacy/classes/local/request/content_writer.php</a> </span>file, I can see that is an interface, while the <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/writer.php" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">/privacy/classes/local/request/writer.php</span></a> is a class described as a "<i>factory class used to fetch and work with the content_writer</i>". So I think the "writer" class has been provided as a shortcut.<br />
<br />
Looking at the exporter code for <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/mod/choice/classes/privacy/provider.php#L102" target="_blank">choice</a> and <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/mod/forum/classes/privacy/provider.php#L260" target="_blank">forum</a>, it appears that there is no specific format for the output of a module. The data is structured as JSON, but the elements seem to be up to the plugin. This makes sense, since any plugin can have very different data.<br />
<br />
For example, a choice activity export looks like this:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;">{
<span style="color: #660e7a; font-weight: bold;">"name"</span>: <span style="color: green; font-weight: bold;">"Choice One"</span>,
<span style="color: #660e7a; font-weight: bold;">"intro"</span>: <span style="color: green; font-weight: bold;">""</span>,
<span style="color: #660e7a; font-weight: bold;">"completion"</span>: {
<span style="color: #660e7a; font-weight: bold;">"state"</span>: <span style="color: blue;">0</span><span style="color: blue;"> </span>
},
<span style="color: #660e7a; font-weight: bold;">"answer"</span>: [
<span style="color: green; font-weight: bold;">"Choice 2"</span><span style="color: green; font-weight: bold;"> </span>
],
<span style="color: #660e7a; font-weight: bold;">"timemodified"</span>: <span style="color: green; font-weight: bold;">"Wednesday, 3 May 2017, 6:28 pm"</span>
}</pre>
</blockquote>
While a forum post looks like this:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;">{
<span style="color: #660e7a; font-weight: bold;">"subject"</span>: <span style="color: green; font-weight: bold;">"My New Post"</span>,
<span style="color: #660e7a; font-weight: bold;">"created"</span>: <span style="color: green; font-weight: bold;">"Friday, 1 June 2018, 3:15 pm"</span>,
<span style="color: #660e7a; font-weight: bold;">"modified"</span>: <span style="color: green; font-weight: bold;">"Friday, 1 June 2018, 3:15 pm"</span>,
<span style="color: #660e7a; font-weight: bold;">"author_was_you"</span>: <span style="color: green; font-weight: bold;">"Yes"</span>,
<span style="color: #660e7a; font-weight: bold;">"message"</span>: <span style="color: green; font-weight: bold;">"<p>Hi. This is my new post. I hope you like it.</p></span>
}</pre>
</blockquote>
Before I implement an exporter, I will need to decide what the data should look like. I'll stick with my simple attempts data for now. Since any questionnaire instance can have multiple attempts by a user, it makes sense to create a structure organized by the instance; in this case the course module id. So my structure should look like this:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;">{
<span style="color: #660e7a; font-weight: bold;">"name"</span>: <span style="color: green; font-weight: bold;">"Questionnaire name"</span>,
<span style="color: #660e7a; font-weight: bold;">"intro"</span>: <span style="color: green; font-weight: bold;">"Complete this questionnaire"</span>,
<span style="color: #660e7a; font-weight: bold;">"completion"</span>: {
<span style="color: #660e7a; font-weight: bold;">"state"</span>: <span style="color: blue;">0</span><span style="color: blue;"> </span>
},
<span style="color: #660e7a; font-weight: bold;">"attempts"</span>: [
{
<span style="color: green; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">"responseid": </span>"rid1",</span>
<span style="color: green; font-weight: bold;"><span style="color: green; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;"> "timemodified"</span>: "Wednesday, 3 May 2017, 6:28 pm"
</span></span>},
{
<span style="color: green; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;">"responseid": </span>"rid2",</span>
<span style="color: green; font-weight: bold;"><span style="color: green; font-weight: bold;"><span style="color: #660e7a; font-weight: bold;"> "timemodified"</span>: "Thursday, 4 May 2017, 9:31 am"
</span></span>}
]
}</pre>
</blockquote>
Looking at the choice activity code for the exporter, I create a function to create the JSON structure I am aiming for. You can see the code <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/6e0f1ed921c29d23e01ac2d8b7c4b0c51adab4ba/classes/privacy/provider.php#L90" target="_blank">here</a>. This code uses several functions provided by the API that are not documented in the wiki. The documentation is really in the class files themselves.<br />
<br />
The following <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/6e0f1ed921c29d23e01ac2d8b7c4b0c51adab4ba/classes/privacy/provider.php#L135" target="_blank">line</a> displays the time and date in a readable form:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: green; font-weight: bold;">'timemodified' </span><span style="background-color: #f7faff;">=> \core_privacy\local\request\transform::</span><span style="background-color: #f7faff; font-style: italic;">datetime</span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$attempt</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">timemodified</span><span style="background-color: #f7faff;">),</span></pre>
</blockquote>
You can find the datetime function in the <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/transform.php#L56" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">/privacy/classes/local/request/transform.php</span></a> file.<br />
<br />
The following <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/6e0f1ed921c29d23e01ac2d8b7c4b0c51adab4ba/classes/privacy/provider.php#L125" target="_blank">line</a> gets a structure containing general data for the activity and user that can be merged with the data more specific to the activity:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: #660000;">$contextdata </span><span style="background-color: #f7faff;">= \core_privacy\local\request\helper::</span><span style="background-color: #f7faff; font-style: italic;">get_context_data</span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$context</span><span style="background-color: #f7faff;">, </span><span style="background-color: #f7faff; color: #660000;">$user</span><span style="background-color: #f7faff;">);</span></pre>
</blockquote>
This function is contained in the file <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/helper.php#L118" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">/privacy/classes/local/request/helper.php</span></a>. Following through that code, it creates the part of the JSON structure I need, prior to the 'attempts' array.<br />
<br />
The <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/6e0f1ed921c29d23e01ac2d8b7c4b0c51adab4ba/classes/privacy/provider.php#L127" target="_blank">following lines</a>, merge the specific data I want to export with the general data and then writes that JSON data to the export function:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: #660000;">$contextdata </span><span style="background-color: #f7faff;">= (object)</span><span style="background-color: #f7faff; font-style: italic;">array_merge</span><span style="background-color: #f7faff;">((array)</span><span style="background-color: #f7faff; color: #660000;">$contextdata</span><span style="background-color: #f7faff;">, </span><span style="background-color: #f7faff; color: #660000;">$attemptdata</span><span style="background-color: #f7faff;">);</span><span style="background-color: #f7faff;">
\core_privacy\local\request\writer::</span><span style="background-color: #f7faff; font-style: italic;">with_context</span><span style="background-color: #f7faff;">(</span><span style="background-color: #f7faff; color: #660000;">$context</span><span style="background-color: #f7faff;">)->export_data([], </span><span style="background-color: #f7faff; color: #660000;">$contextdata</span><span style="background-color: #f7faff;">);</span></pre>
</blockquote>
The <span style="font-family: "courier new" , "courier" , monospace;">with_context</span> function is contained in the file <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/writer.php#L94" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">/privacy/classes/local/request/writer.php</span></a>, and calls the <span style="font-family: "courier new" , "courier" , monospace;">export_data</span> function which is ultimately located in the <a href="https://github.com/moodle/moodle/blob/MOODLE_35_STABLE/privacy/classes/local/request/moodle_content_writer.php#L84" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">/privacy/classes/local/request/moodle_content_writer.php</span></a> file.<br />
<br />
The end result of this is an exported structure in JSON form.<br />
<br />
Now, to test this, Moodle has <a href="https://docs.moodle.org/dev/Privacy_API/Utilities" target="_blank">provided some scripts</a> that can be created and executed from the CLI. The one I want to use is the "Test of exporting user data" script, provided on that page. So, I create that script on my test site, and execute it. When I execute it, there is a lot of output. Scanning through the output, I see:<br />
<blockquote class="tr_bq">
"<i>Processing mod_questionnaire (4/15) (Friday, 1 June 2018, 8:37 pm)</i>"</blockquote>
which is positive.<br />
<br />
And the last line says:<br />
<blockquote class="tr_bq">
"<i>== File export was uncompressed to /moodledevsite/moodledata/temp/privacy/3d5750c5-4d5b-4e96-9e86-663cbc9ed177</i>".</blockquote>
<br />
This means that there is data located in my moodledata directory, that should contain the exported data. A visual structure of that area looks like this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN_Yiz41JiWgJFtuWSYyXduR0FORLHkQlNtLPSFVea6hy2GKLdJOUfuZ4t8qM02tyAH9pD4fIpoRWbl-4khuWk7eIDFEENrccwexid7iGkLGTxrYAxgol84og1lDYZ6EopBbiIt4KjyrXa/s1600/Screen+Shot+2018-06-01+at+3.41.48+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="516" data-original-width="447" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN_Yiz41JiWgJFtuWSYyXduR0FORLHkQlNtLPSFVea6hy2GKLdJOUfuZ4t8qM02tyAH9pD4fIpoRWbl-4khuWk7eIDFEENrccwexid7iGkLGTxrYAxgol84og1lDYZ6EopBbiIt4KjyrXa/s400/Screen+Shot+2018-06-01+at+3.41.48+PM.png" width="346" /></a></div>
<br />
I have opened it to the questionnaire I am testing. The "data.json" file will contain the data I exported. When I open the JSON file, I see:<br />
<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;">{
<span style="color: #660e7a; font-weight: bold;">"name"</span>: <span style="color: green; font-weight: bold;">"Test Questionnaire"</span>,
<span style="color: #660e7a; font-weight: bold;">"intro"</span>: <span style="color: green; font-weight: bold;">"<div</span><span style="color: green; font-weight: bold;">><p>A wonderful description of the questionnaire.<</span><span style="color: navy; font-weight: bold;">\/</span><span style="color: green; font-weight: bold;">p><</span><span style="color: navy; font-weight: bold;">\/</span><span style="color: green; font-weight: bold;">div>"</span>,
<span style="color: #660e7a; font-weight: bold;">"completion"</span>: {
<span style="color: #660e7a; font-weight: bold;">"state"</span>: <span style="color: green; font-weight: bold;">"1"</span><span style="color: green; font-weight: bold;"> </span>
},
<span style="color: #660e7a; font-weight: bold;">"attempts"</span>: [
{
<span style="color: #660e7a; font-weight: bold;">"responseid"</span>: <span style="color: green; font-weight: bold;">"66"</span>,
<span style="color: #660e7a; font-weight: bold;">"timemodified"</span>: <span style="color: green; font-weight: bold;">"Friday, 18 November 2016, 8:14 pm"</span><span style="color: green; font-weight: bold;"> </span>
},
{
<span style="color: #660e7a; font-weight: bold;">"responseid"</span>: <span style="color: green; font-weight: bold;">"88"</span>,
<span style="color: #660e7a; font-weight: bold;">"timemodified"</span>: <span style="color: green; font-weight: bold;">"Tuesday, 11 April 2017, 8:50 pm"</span><span style="color: green; font-weight: bold;"> </span>
},
{
<span style="color: #660e7a; font-weight: bold;">"responseid"</span>: <span style="color: green; font-weight: bold;">"89"</span>,
<span style="color: #660e7a; font-weight: bold;">"timemodified"</span>: <span style="color: green; font-weight: bold;">"Tuesday, 11 April 2017, 8:54 pm"</span><span style="color: green; font-weight: bold;"> </span>
}
]
}</pre>
</blockquote>
Which appears to match what I wanted.<br />
<br />
That's some good progress. In <a href="http://tandl.churchward.ca/2018/06/implementing-moodles-privacy-api-in_6.html" target="_blank">Part 3</a>, I'll add the delete data portion of the API.<br />
<br />
<br />
<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Mike Churchwardhttp://www.blogger.com/profile/07504877392767978258noreply@blogger.com1tag:blogger.com,1999:blog-7094986855457289958.post-68662192440566197232018-05-31T17:18:00.000-04:002018-06-04T10:31:28.266-04:00Implementing Moodle's Privacy API in a Moodle Plugin - Part 1The General Data Protection Regulation, or GDPR has now come into effect. Essentially it is a regulation in EU law designed to protect the privacy of online data for individuals within the EU. As such, any online service providers who work within the EU, or have members from the EU, need to address their online data privacy. I won't go into detail about what this regulation is, or how it is interpreted legally, in any articles here, but you can read about it online. <a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation" target="_blank">Wikipedia has an overview</a>.<br />
<br />
For a Moodle plugin developer, this means ensuring your plugin handles user data in accordance with this regulation. A plugin needs to be able to provide the data it stores for an individual user to that user upon request, and it needs to be able to remove a specific user's data if requested. Fortunately, Moodle has provided an API for plugin developers to do all of the heavy lifting for plugins. In this article, I will begin to learn about this API and implement it in a plugin, with the goal of making my questionnaire plugin GDPR compliant.<br />
<br />
A great place to start is this <a href="https://youtu.be/jcVKzq2qimQ" target="_blank">video</a> that Moodle HQ put together, featuring the core developer <a href="https://moodle.org/user/view.php?id=268794&course=5" target="_blank">Andrew Nicols</a> explaining how to go about implementing this API. There is also the main <a href="https://docs.moodle.org/dev/Privacy_API" target="_blank">Privacy API</a> documentation and the <a href="https://docs.moodle.org/dev/Subject_Access_Request_FAQ" target="_blank">Subject Access Request FAQ</a>.<br />
<br />
To begin, I'll create a new branch for this work based on the 3.5 stable branch called <a href="https://github.com/PoetOS/moodle-mod_questionnaire/tree/M35_PRIVACY_API" target="_blank">M35_PRIVACY_API</a>. My work for this will be tested on a Moodle 3.5 site.<br />
<br />
First step is to determine if the plugin actually contains personal data. The privacy API defines what it considers to be personal data in the <a href="https://docs.moodle.org/dev/Privacy_API#Personal_data_in_Moodle" target="_blank">documentation</a>. I think the following paragraph pretty much indicates that the questionnaire has personal data about each user that completes one:<br />
<blockquote class="tr_bq">
<i>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.</i></blockquote>
The questionnaire stores a user's response to all of its questions with a timestamp and the specific answers. This data is definitely personal data.<br />
<br />
The documentation indicates that my plugin must <a href="https://docs.moodle.org/dev/Privacy_API#Implementing_a_provider" target="_blank">implement a relevant metadata and request provider</a>. To do this, I must create a class in the namespace <span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire\privacy</span> in a file named <span style="font-family: "courier new" , "courier" , monospace;">mod/questionnaire/classes/privacy/provider.php</span>. And, since my plugin does store personal data, the <span style="font-family: "courier new" , "courier" , monospace;">provider</span> class must implement the <span style="font-family: "courier new" , "courier" , monospace;">\core_privacy\local\metadata\provider</span> interface. In order for it to export and delete user data it must also implement a request provider. For an activity plugin, I should implement <span style="font-family: "courier new" , "courier" , monospace;">\core_privacy\local\request\plugin\provider</span> for the request provider. <br />
<blockquote class="tr_bq">
<span style="background-color: #fff2cc;"><i>Note that there are other request providers your plugin might need, depending on whether they use other Moodle systems. The documentation talks about subsystems like ratings and tags, user preferences, and subplugins. Each of these has a different request provider interface that should be implemented. David Mudrack pointed out a <a href="https://github.com/moodle/moodle/blob/master/privacy/classes/manager.php#L37-L106" target="_blank">document that listed these interfaces</a> which helps.</i></span></blockquote>
To be complete, my new class must implement the <span style="font-family: "courier new" , "courier" , monospace;">get_metadata</span>, <span style="font-family: "courier new" , "courier" , monospace;">get_contexts_for_userid</span>, <span style="font-family: "courier new" , "courier" , monospace;">export_user_data</span>, <span style="font-family: "courier new" , "courier" , monospace;">delete_data_for_all_users_in_context</span>, and the <span style="font-family: "courier new" , "courier" , monospace;">delete_data_for_user</span> functions.<br />
<br />
So, that becomes my first step. I create the <span style="font-family: "courier new" , "courier" , monospace;">mod/questionnaire/classes/privacy/provider.php</span> file as follows:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;"><?php<br />namespace mod_questionnaire\privacy;<br /><br />defined('MOODLE_INTERNAL') || die();<br /><br />class provider implements<br /> \core_privacy\local\metadata\provider,<br /> \core_privacy\local\request\plugin\provider {<br /><br /> public static function get_metadata(\core_privacy\local\metadata\collection $collection):<br /> \core_privacy\local\metadata\collection {<br /> return $collection;<br /> }<br /><br /> public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {<br /> $contextlist = new \core_privacy\local\request\contextlist();<br /> return $contextlist;<br /> }<br /><br /> public static function export_user_data(\core_privacy\local\request\approved_contextlist $contextlist) {}<br /><br /> public static function delete_data_for_all_users_in_context(\context $context) {}<br /><br /> public static function delete_data_for_user(\core_privacy\local\request\approved_contextlist $contextlist) {}<br />}</span> </blockquote>
This gives me the <a href="https://github.com/PoetOS/moodle-mod_questionnaire/blob/27bdd6531126c8364a564dbaafa0f3aa14e67509/classes/privacy/provider.php" target="_blank">basic skeleton</a> to work from.<br />
<br />
At this point, I can verify if the plugin API is seen by the Moodle site. To do this, I copy the new work into my development site's plugin directory. I can then go to the "Plugin privacy registry" page of the site, to see if my plugin shows up. This page is in the "Site administration / Users / Privacy and policies" section. On this page, I can open the "Activity module" section and scroll down until I see my questionnaire plugin. If it doesn't have a non-compliant icon next to it, then I have succeeded in making the API visible to Moodle. When I look, I see that I am moving in the right direction. The image below shows my plugin, and another plugin that does not have the API defined yet (on my site anyway).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDDBAnQzwwN71JkoJkj2ePU7WpURiOPJGVflXv4CTJbUvyioHG2mInytZ0degucVTG_1pecir-aJOYpIexpWH_i1SePd7f6joR09ijqfeJMzbmnCfOffJsCs6l66207HnY7HxwZhQ4INym/s1600/Screen+Shot+2018-05-31+at+5.07.40+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="390" data-original-width="452" height="276" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDDBAnQzwwN71JkoJkj2ePU7WpURiOPJGVflXv4CTJbUvyioHG2mInytZ0degucVTG_1pecir-aJOYpIexpWH_i1SePd7f6joR09ijqfeJMzbmnCfOffJsCs6l66207HnY7HxwZhQ4INym/s320/Screen+Shot+2018-05-31+at+5.07.40+PM.png" width="320" /></a></div>
<br />
In <a href="http://tandl.churchward.ca/2018/06/implementing-moodles-privacy-api-in.html">part 2</a>, I'll begin adding the code to complete this work.<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Mike Churchwardhttp://www.blogger.com/profile/07504877392767978258noreply@blogger.com4tag:blogger.com,1999:blog-7094986855457289958.post-49877274300352453792018-03-05T10:34:00.000-05:002018-03-05T10:34:24.970-05:00Looking at Moodle's new plugin development API's - Part 1David Mudrack, of Moodle HQ released a plugin called the <a href="https://github.com/mudrd8mz/moodle-block_todo" target="_blank">"My Todo List" block</a>. While functional, its real purpose is to show off the latest advanced coding techniques available in recent versions of Moodle. See his <a href="https://twitter.com/mudrd8mz/status/949332754281500673" target="_blank">Tweet</a>. From the readme:<br />
<blockquote class="tr_bq">
<i>The main purpose of this plugin is to demonstrate usage of advanced coding techniques
available in recent Moodle versions. Most notably:</i><br />
<ul>
<li><i>Rendering HTML output via Mustache templates.</i></li>
<li><i>AJAX based workflow of the elementary CRUD operations.</i></li>
<li><i>Organising JS into AMD modules.</i></li>
<li><i>Organising external functions into traits.</i></li>
<li><i>Low-level access to the database via persistent models.</i></li>
<li><i>Using exporters for handling the data structures in rendering and AJAX.</i></li>
</ul>
</blockquote>
For this post, I will install and examine the code with the goal of learning better how to use these concepts.<br />
<br />
While I have been a plugin developer for many years, I have not been able to keep up with all of the latest techniques to enhance the plugins I maintain. I am personally really interested in getting better with AJAX and AMD, as many of my plugins could benefit from these features. I have already experimented and released code with renderers and Mustache templates (see the series beginning <a href="http://tandl.churchward.ca/2017/01/adding-renderers-and-templates-to-your.html" target="_blank">here</a>), so I won't go deep into those unless I see something new.<br />
<br />
To begin, I fork <a href="https://github.com/mudrd8mz/moodle-block_todo" target="_blank">David's repository</a> into my own <a href="https://github.com/PoetOS/moodle-block_todo" target="_blank">Github repository</a>. This gives me place to install and play with the code. Once I have a local copy, I load it into my IDE (I use PhpStorm) and take a look at the code.<br />
<br />
A quick perusal of the code shows that this is indeed a simple plugin, and uses the techniques I'm most interested in! The main block code file, <span style="font-family: "courier new" , "courier" , monospace;">block_todo.php</span>, contains five methods, four of which I'm familiar with from <a href="https://docs.moodle.org/dev/Blocks" target="_blank">standard block development</a>: <span style="font-family: "courier new" , "courier" , monospace;">init()</span>, <span style="font-family: "courier new" , "courier" , monospace;">get_content()</span>, <span style="font-family: "courier new" , "courier" , monospace;">specialization()</span>, and <span style="font-family: "courier new" , "courier" , monospace;">applicable_formats()</span>. But the fifth, <a href="https://github.com/PoetOS/moodle-block_todo/blob/master/block_todo.php#L83" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">get_required_javascript()</span></a>, I am not familiar with and it is not defined on the block development page. I suspect this has to do with the AMD module feature. And, while that may be true, it appears that it calls the parent method, meaning this function is part of the block class. I need to do some searching to see why this function is there.<br />
<br />
I search the development documentation on Moodledocs, and find this function referred to on the <a href="https://docs.moodle.org/dev/jQuery_pre2.9#jQuery_UI_in_add-on_block" target="_blank">"jQuery pre2.9" page</a>. So, it must be a function that has remained even with the new AMD module addition. I think I may want to look for the AMD Module documentation first.<br />
<br />
I find a page in the Moodle development docs wiki on <a href="https://docs.moodle.org/dev/Javascript_Modules" target="_blank">Javascript Modules</a>. This appears to be the main documentation for using AMD in plugins. Skimming through I see that initializing an AMD javascript function is done with the <span style="font-family: "courier new" , "courier" , monospace;">$this->page->requires->js_call_amd()</span> function. This is done in the ToDo block's <a href="https://github.com/PoetOS/moodle-block_todo/blob/master/block_todo.php#L86" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">get_required_javascript</span></a> method.<br />
<br />
A closer look at the parent block class, defines the get_required_javascript method as:<br />
<blockquote class="tr_bq">
<i>Allows the block to load any JS it requires into the page.<br />By default this function simply permits the user to dock the block if it is dockable.</i></blockquote>
So, I'm not sure that this function is actually required, but may just be a convenient place to call the <span style="font-family: "courier new" , "courier" , monospace;">js_call_amd</span> function from. I search all of the core blocks for <span style="font-family: "courier new" , "courier" , monospace;">js_call_amd</span> and find two, both of which call it from the <span style="font-family: "courier new" , "courier" , monospace;">get_required_javascript</span> method. So this seems to be the place to do that. Maybe later, I'll play with that to see if that is a required way to do it.<br />
<br />
In any case, the function passes the arguments:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">'block_todo/control', 'init', ['instanceid' => $this->instance->id]</span></blockquote>
This coincides with the file <a href="https://github.com/PoetOS/moodle-block_todo/blob/master/amd/src/control.js" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">amd/src/control.js</span></a>, which is the expected location within a plugin to find the AMD javascript files.<br />
<br />
Getting back to the function of the plugin, the main block <span style="font-family: "courier new" , "courier" , monospace;">get_content</span> function, is a very simple function that does two things: gets a list of todo items, and outputs them on the screen. The technique uses one technique I'm familiar with, <a href="https://docs.moodle.org/dev/Templates#Templates" target="_blank">templates</a>, and two I am unfamiliar with, <a href="https://docs.moodle.org/dev/Persistent" target="_blank">Moodle persistents</a> and <a href="https://docs.moodle.org/dev/Exporter" target="_blank">Moodle exporters</a>.<br />
<br />
This is what I am seeing in <a href="https://github.com/PoetOS/moodle-block_todo/blob/master/block_todo.php#L49" target="_blank"><span style="font-family: "courier new" , "courier" , monospace;">get_content</span></a>:<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: grey; font-style: italic;">// Load the list of persistent </span><span style="background-color: #f7faff; color: #0073bf; font-style: italic; font-weight: bold;">todo item models from the database.</span>
<span style="background-color: #f7faff; color: #660000;">$items </span><span style="background-color: #f7faff;">= block_todo\item::</span><span style="background-color: #f7faff; font-style: italic;">get_my_todo_items</span><span style="background-color: #f7faff;">();</span></pre>
</blockquote>
This code uses the persistent class, which appears to be a new technique to manage database data using CRUD techniques. It will be interesting to learn why this exists and when it should be used.<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: grey; font-style: italic;">// Prepare the exporter of the </span><span style="background-color: #f7faff; color: #0073bf; font-style: italic; font-weight: bold;">todo items list.</span>
<span style="background-color: #f7faff; color: #660000;">$list </span><span style="background-color: #f7faff;">= </span><span style="background-color: #f7faff; color: navy; font-weight: bold;">new </span><span style="background-color: #f7faff;">block_todo\external\list_exporter([</span>
<span style="background-color: #f7faff;"> </span><span style="background-color: #f7faff; color: green; font-weight: bold;">'instanceid' </span><span style="background-color: #f7faff;">=> </span><span style="background-color: #f7faff; color: #660000;">$this</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">instance</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">id</span>
<span style="background-color: #f7faff;">,</span>
<span style="background-color: #f7faff;">], [</span>
<span style="background-color: #f7faff;"> </span><span style="background-color: #f7faff; color: green; font-weight: bold;">'items' </span><span style="background-color: #f7faff;">=> </span><span style="background-color: #f7faff; color: #660000;">$items</span><span style="background-color: #f7faff;">,</span>
<span style="background-color: #f7faff;"> </span><span style="background-color: #f7faff; color: green; font-weight: bold;">'context' </span><span style="background-color: #f7faff;">=> </span><span style="background-color: #f7faff; color: #660000;">$this</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">context</span>
<span style="background-color: #f7faff;">,</span><span style="background-color: #f7faff;">]);</span></pre>
</blockquote>
This code uses exporter class and appears to be a technique to manage data passed in and out of web service functions, used by external applications and AJAX.<br />
<blockquote class="tr_bq">
<pre style="background-color: white; color: black; font-family: 'Menlo'; font-size: 9.0pt;"><span style="background-color: #f7faff; color: grey; font-style: italic;">// Render the list using a template and exported data.</span>
<span style="background-color: #f7faff; color: #660000;">$this</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">content</span><span style="background-color: #f7faff;">-></span><span style="background-color: #f7faff; color: #660e7a; font-weight: bold;">text </span><span style="background-color: #f7faff;">=</span><span style="background-color: #f7faff; color: #660000;"> $OUTPUT</span><span style="background-color: #f7faff;">->render_from_template(</span><span style="background-color: #f7faff; color: green; font-weight: bold;">'block_todo/content'</span><span style="background-color: #f7faff;">,
</span><span style="background-color: #f7faff; color: #660000;">$list</span><span style="background-color: #f7faff;">->export(</span><span style="background-color: #f7faff; color: #660000;">$OUTPUT</span><span style="background-color: #f7faff;">));</span></pre>
</blockquote>
This code is using a Mustache template to display the block content and utilizes the exporter class to send the data rather than renderers defined for the plugin, as I am used to. Again, I will need to discover why this technique is used.<br />
<br />
I have some concepts to learn, so I install the block just to see what it does and how it works. After adding it to my dashboard page, I play with it, and see that it is as advertises, a simple "To Do" list.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5BUW31_JAMQsYTru7k6SBtWz3mPEJ3WuPZVCUQHHNZCPVoeBTJXo0_4Qn5zHpBTeotYfguK38Mb5me9vzlV4R-rbbS5PC1eiVHrjM5h-XiU1HGf067L7CiAV1qPMoK87vX3ulB8ONryCK/s1600/Screen+Shot+2018-03-05+at+10.08.43+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="232" data-original-width="532" height="173" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5BUW31_JAMQsYTru7k6SBtWz3mPEJ3WuPZVCUQHHNZCPVoeBTJXo0_4Qn5zHpBTeotYfguK38Mb5me9vzlV4R-rbbS5PC1eiVHrjM5h-XiU1HGf067L7CiAV1qPMoK87vX3ulB8ONryCK/s400/Screen+Shot+2018-03-05+at+10.08.43+AM.png" width="400" /></a></div>
<br />
Hovering over the controls shows no obvious links, and a quick perusal of the page code shows a form, but not a standard functioning Moodle form. It looks like all of the controls are using Javascript, AJAX and web services to do the actual work. This will be all new to me. :-)<br />
<br />
Next post, I will start my functional learning.<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Mike Churchwardhttp://www.blogger.com/profile/07504877392767978258noreply@blogger.com3tag:blogger.com,1999:blog-7094986855457289958.post-18275906748787056892018-02-12T11:23:00.000-05:002018-02-12T11:23:03.376-05:00The Road AheadIn October of 2017, I left the employment of Remote Learner. While I had technically not been working for them for almost two years, running Poet Open Source (previously The POET Group), my funding was coming directly from them. Our parting was civil, and they remain a company fully supportive of open source technologies such as Moodle and now Totara.<br />
<br />
So where does that leave me? Well, I haven’t completely decided what I want to do with the rest of my life - but to be fair, I never really have. ;-)<br />
<br />
I remain a firm believer in open source and its ability to strengthen, change and improve the world.<br />
<br />
Since my discovery of open source software over fifteen years ago, and Moodle shortly after that, I have become a supporter and proponent of free ideas, open collaboration and sharing of results. I believe that open source ideals can be applied to almost any solution, innovation and need - not just software.<br />
<br />
Going forward, I would like to see Poet Open Source grow as an organization that champions and helps move important projects forward, using open source methods and ideals. I would like to collaborate with and help organize like minded individuals and organizations. What and how Poet works on will be part of that journey.<br />
<br />
Moodle will remain to be an important part of my and Poet’s journey; I continue to develop and maintain important Moodle plugins, and I am on the Moodle Users Association committee. But I also believe there is more Poet can do, and will strive to find and contribute to those things.<br />
<br />
If you’re interested in the same and would like to join the discussion, drop me a line.<br />
<div>
<br /></div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com6tag:blogger.com,1999:blog-7094986855457289958.post-83976121918628916972017-05-19T15:38:00.000-04:002017-05-19T15:38:15.395-04:00Things I learned upgrading to Moodle 3.3As the Moodle 3.3 launch approached, I decided to check on a few of my plugins and ensure they were ready for 3.3.<br />
<br />
The good news is that having had everything pretty much up to 3.2 standards, the effort to reach 3.3 was minimal. In fact, with the two plugins I worked on for the coveted "<a href="https://moodle.org/plugins/browse.php?list=award&id=8">Early bird 3.3</a>" award, <a href="https://moodle.org/plugins/mod_questionnaire">questionnaire module</a> and <a href="https://moodle.org/plugins/filter_oembed">oembed filter</a>, I could have submitted them as is and they would have passed fine. But, that's not my way, and besides, it would have made this post even shorter than it is.<br />
<br />
For the questionnaire module, I first installed the 3.2 version on a new Moodle 3.3 test site, and ran the unit tests and Behat tests. Having automated testing is a great way to manage upgrading, as these tests will hopefully uncover any problems introduced by the Moodle upgrade or by any changes made to the plugin for the new version. In the questionnaire case, I had made a number of improvements to the code, continuing my efforts of paying down "technical debt" and making the code easier to maintain going forward.<br />
<br />
The unit tests ran fine, but the Behat tests failed. Additionally, there were messages being kicked out by the developer level debugging.<br />
<br />
There were two issues in Behat I needed to fix. The first is a change to the Behat tests in Moodle 3.3. Where previously a step would say:<br />
<blockquote class="tr_bq">
<i>'And I follow "Course 1"' </i></blockquote>
For 3.3, they need to say:<br />
<blockquote class="tr_bq">
<i>'And I am on "Course 1" course homepage'</i></blockquote>
This is due to the new course dashboard introduced in 3.3 that replaces some of the navigation in 3.2.<br />
<br />
The other issue I needed to change was places that would say:<br />
<blockquote class="tr_bq">
<i>'I navigate to "Questions" node in "Questionnaire administration"'</i></blockquote>
Now need to say:<br />
<blockquote class="tr_bq">
<i>'I navigate to "Questions" in current page administration'</i></blockquote>
This was actually a change in 3.2 that didn't affect me until 3.3. I believe I was supposed to have made the change in 3.2, but now it is mandatory.<br />
<br />
Making those changes fixed the Behat errors.<br />
<br />
The debugging warning I was getting was:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">pix_url is deprecated. Use image_url for images and pix_icon for icons.<br /> line 267 of /lib/outputrenderers.php: call to debugging()<br /> line 182 of /mod/questionnaire/classes/questions_form.php: call to renderer_base->pix_url()<br /> line 204 of /lib/formslib.php: call to mod_questionnaire_questions_form->definition()<br /> line 32 of /mod/questionnaire/classes/questions_form.php: call to moodleform->__construct()<br /> line 153 of /mod/questionnaire/questions.php: call to mod_questionnaire_questions_form->__construct()</span></blockquote>
This is telling me that places I am using the function <span style="font-family: Courier New, Courier, monospace;">pix_url</span> will eventually (in some future release) fail. And they should be replaced with either <span style="font-family: Courier New, Courier, monospace;">pix_icon</span> or <span style="font-family: Courier New, Courier, monospace;">image_url</span>. For my case, <span style="font-family: Courier New, Courier, monospace;">image_url</span> is the easiest and correct fix. The previous function, <span style="font-family: Courier New, Courier, monospace;">pix_url</span>, returned an URL to be used in the output. The new function, <span style="font-family: Courier New, Courier, monospace;">image_url</span>, likewise returns an URL. The new function <span style="font-family: Courier New, Courier, monospace;">pix_icon</span> returns the entire line of code including the image tags. Using it would require a significant amount or re-coding, and in reality, I am not using icons.<br />
<br />
Once I have completed those changes, everything seems fine with questionnaire.<br />
<br />
For the Oembed filter, I ran into Behat issues caused by the HTML that is output with 3.3. There has been changes made to some of the core editing features, that changed the <span style="font-family: Courier New, Courier, monospace;">xpath</span> layout for the Oembed editing page. As such, I was getting an error like:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">001 Scenario: Admin user carries out various provider management tasks. # /Users/mikechurchward/www/moodlehq.git/filter/oembed/tests/behat/management.feature:26<br /> And the provider "Vimeo" is disabled # /Users/mikechurchward/www/moodlehq.git/filter/oembed/tests/behat/management.feature:36<br /> The "//td/a[text()='Vimeo']/parent::td/div/a[contains(@class,'filter-oembed-visibility')]/i[@title='Show']" element does not exist and should exist (Behat\Mink\Exception\ExpectationException)</span></blockquote>
The Behat tests for the filter had a custom test called "<span style="font-family: Courier New, Courier, monospace;">the_provider_is_disabled</span>" which depended on a specific <span style="font-family: Courier New, Courier, monospace;">xpath</span> output that was now no longer happening. This required me to rewrite the Behat function to change the <span style="font-family: Courier New, Courier, monospace;">xpath</span> definition to reflect the new output. Manual testing of the function proved that the actual functionality had not been broken, and once the new <span style="font-family: Courier New, Courier, monospace;">xpath</span> definition was in place, the Behat tests passed as well.<br />
<div>
<br />
And that was it. Both plugins were ready for 3.3 and made it in time to get the early bird award. As I continue with other plugins, I will document any new changes required by 3.3 I find in new posts.</div>
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com4tag:blogger.com,1999:blog-7094986855457289958.post-36683899561771929182017-04-21T15:36:00.000-04:002017-04-21T15:36:36.339-04:00Add mobile support to your Moodle plugin - part four<h3>
Part Four - Adding More Services</h3>
In the previous part of this series, I modified my mobile addon so that clicking on a plugin instance loaded and displayed a "Hello World!" screen. Now, I will add some services so that it will retrieve the number of responses made to the particular questionnaire and display them, when a user clicks on an instance. I'm also going to add a link handler, that will handle the questionnaire when it is selected by a link in other site content.<br />
<br />
First, I'm going to add the link function. In all of the other addons I've looked at, a handler was defined for links. In the moodle.org forum, <a href="https://moodle.org/mod/forum/discuss.php?d=349914" target="_blank">I asked about this</a>, and learned that it was so the mobile plugin was handled properly if it was reached from a link other than the course instance.<br />
<br />
Adding the link function is apparently very easy now. The latest release of the mobile app has a simplified handler function that manages the links for me. I really just have to add one line to my <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_LINKHANDLER/addons/main.js#L41">main.js</a></span> file and one line to my <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_LINKHANDLER/addons/services/handlers.js#L74" target="_blank">services/handlers.js</a></span> file, as follows:<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">main.js</span>:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">.config(function($mmCourseDelegateProvider, $mmContentLinksDelegateProvider) {</span><span style="font-family: Courier New, Courier, monospace;"> $mmCourseDelegateProvider.registerContentHandler( 'mmaModQuestionnaire', 'questionnaire', '$mmaModQuestionnaireHandlers.courseContent');</span><span style="font-family: Courier New, Courier, monospace;"> <b>$mmContentLinksDelegateProvider.registerLinkHandler( 'mmaModQuestionnaire', '$mmaModQuestionnaireHandlers.linksHandler')</b>;</span></blockquote>
<span style="font-family: Courier New, Courier, monospace;">services/handlers.js</span>:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">self.linksHandler = $mmContentLinksHelper.createModuleIndexLinkHandler( 'mmaModQuestionnaire', 'questionnaire', $mmaModQuestionnaire);</span></blockquote>
Now, when I create a link in my course that goes to a questionnaire instance, instead of launching in the browser, it uses the questionnaire mobile plugin.<br />
<br />
In the last iteration, I left the <span style="font-family: Courier New, Courier, monospace;">isPluginEnabled</span> function in a state where it always returned <span style="font-family: Courier New, Courier, monospace;">true</span>. Now, I need to make it do something that actually checks with the site to see if the plugin really is enabled. This will require changing this function in the mobile plugin, and adding a web service to the questionnaire module.<br />
<br />
Both the certificate and feedback call the '<span style="font-family: Courier New, Courier, monospace;">mod_[pluginname]_get_[pluginname]_by_courses</span>' web service, so I'll start there. Note that feedback also calls <span style="font-family: Courier New, Courier, monospace;">mod_feedback_get_feedback</span> service, but I'll look at that later.<br />
<br />
Starting with the mobile plugin, I'll update the <span style="font-family: "courier new" , "courier" , monospace;">services/questionnaire.js::isPluginEnabled</span> function as follows:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">self.isPluginEnabled = function(siteId) {<br /> siteId = siteId || $mmSite.getId();</span> </blockquote>
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;"> return $mmSitesManager.getSite(siteId).then(function(site) {<br /> return site.wsAvailable( 'mod_questionnaire_get_questionnaires_by_courses');<br /> });<br />};</span></blockquote>
What this code does, is verify that the questionnaire module is enabled on the Moodle site by checking for the availability of the web service, <span style="font-family: Courier New, Courier, monospace;">mod_quesrionnaire_get_questionnaire_by_courses</span>, on the Moodle site. This means I will need to create that web service in the main questionnaire activity plugin. For this one, I am going to copy from the <a href="https://github.com/jleyva/moodle-mod_certificate/blob/CONTRIB-6313/classes/external.php" target="_blank">certificate module</a>, but simplify it down for now to only provide the basic module information and only check for 'view' capabilities.<br />
<br />
To add the web service to my module, I do the following:<br />
<br />
<b>Add the necessary external services to the <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/classes/external.php#L41">classes/external.php</a></span> file:</b><br />
<br />
This requires creating the file with the <span style="font-family: Courier New, Courier, monospace;">external</span> class extending the core <span style="font-family: Courier New, Courier, monospace;">external_api</span> class in the <span style="font-family: Courier New, Courier, monospace;">mod_questionnaire</span> namespace. In that class, I need to add the function for <span style="font-family: Courier New, Courier, monospace;">get_questionnaires_by_courses</span> as well as two other helper functions, <span style="font-family: Courier New, Courier, monospace;">get_questionnaires_by_courses_parameters</span> and <span style="font-family: Courier New, Courier, monospace;">get_questionnaires_by_courses_returns</span>. These three functions are required to fully define the web service as described in the web services <a href="https://docs.moodle.org/dev/Adding_a_web_service_to_a_plugin#Write_the_external_function_descriptions">developer documentation</a>.<br />
<br />
It is the job of this function to return result information for all questionnaire instances in the identified courses. You can see the full code for this <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/classes/external.php#L61">here</a>.<br />
<br />
<b>Add the <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/db/services.php">db/services.php</a></span> file to define the services available by web services:</b><br />
<br />
Web services need to be described in this file. When a plugin is installed or upgraded, the Moodle system uses this file to add the services described in this file to its known, available services. This is more fully described <a href="https://docs.moodle.org/dev/Adding_a_web_service_to_a_plugin#Declare_the_web_service_function">here</a>. Once the Moodle site knows about them, it can allow them to be used by external systems.<br />
<br />
Since I already have the questionnaire module installed on my Moodle site, I will need to bump up the version number in my <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/version.php#L28">version.php</a></span> file, to trigger a module upgrade, and get the Moodle site to load my new web service. Once I have done this, I should be able to see that my new service has been installed by going to my site's <span style="font-family: Courier New, Courier, monospace;">admin/webservice/service_functions.php</span> screen and looking for the web service there. I upgrade my site with new version and check for the service on the admin screen. Successfully, I see the following:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2a7l3OzR90zJRh7qb_b_-HPvYc8DespUhjTSB6UKsoL5D7FCYyK7jUWaM-t9NSd66xhrZsKbbopxC7jOvOhzekNbItvZTn9XBvLcDmaU504D85gDViKprAlaia9FfSnsWHljYL5F0H6U/s1600/Screen+Shot+2017-04-21+at+2.49.17+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="91" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2a7l3OzR90zJRh7qb_b_-HPvYc8DespUhjTSB6UKsoL5D7FCYyK7jUWaM-t9NSd66xhrZsKbbopxC7jOvOhzekNbItvZTn9XBvLcDmaU504D85gDViKprAlaia9FfSnsWHljYL5F0H6U/s400/Screen+Shot+2017-04-21+at+2.49.17+PM.png" width="400" /></a></div>
<br />
Since I'm writing web services, I might as well create one that provides some meaningful information from the Moodle site that I can display in my mobile plugin.<br />
<br />
Currently, when I click on a questionnaire in the mobile app, I just get the "Hello World!" screen. I am going to modify it so that it tells me how many responses I have made to that instance. To do that, I will need a web service in the Moodle plugin to provide that information back to the mobile plugin.<br />
<br />
Back at my <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/db/services.php#L36">db/services.php</a></span> file, I define a new service called '<span style="font-family: Courier New, Courier, monospace;">mod_questionnaire_get_user_responses</span>' and define it as a service that returns 'a count of the current user responses'.<br />
<br />
Then, I code the new service in my <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/classes/external.php#L142">classes/external.php</a></span> file. Its a simple function, that simply returns the count of responses for the user / questionnaire instance in question.<br />
<br />
Lastly, I perform a version bump to force an upgrade and verify that the service has been added to the Moodle site:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMEr3Zbh_3PSwLVDJsjT-t0MiDUWqvlVW7FM_KjX70-Vgt55rdG6bgf79WfjSRLjJw4C9dkBQChKjJndd2j-mwg0LLmfKSCu8xHf6KLxnzbG3LY2lk04zCHEqQM1kqr5uUkZJaNQmC21M/s1600/Screen+Shot+2017-04-21+at+3.07.59+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="38" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMEr3Zbh_3PSwLVDJsjT-t0MiDUWqvlVW7FM_KjX70-Vgt55rdG6bgf79WfjSRLjJw4C9dkBQChKjJndd2j-mwg0LLmfKSCu8xHf6KLxnzbG3LY2lk04zCHEqQM1kqr5uUkZJaNQmC21M/s400/Screen+Shot+2017-04-21+at+3.07.59+PM.png" width="400" /></a></div>
<br />
Now, I need to modify the mobile plugin to use this new service and display the information appropriately. Starting with <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/addons/templates/index.html">templates/index.html</a></span> file, I modify so that it will display a count of the user's responses:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;"><ion-view><br /> <ion-nav-title>{{ title }}</ion-nav-title><br /> <ion-content padding="true" mm-state-class><br /> <mm-course-mod-description description="description"></mm-course-mod-description><br /> {{mymessage}}<br /><br /> You have {{responses}} responses.<br /> </ion-content><br /></ion-view></span></blockquote>
The template will look for a value in the <span style="font-family: Courier New, Courier, monospace;">{{responses}}</span> tag, which means I need to add code to the <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/addons/controllers/index.js#L60">controllers/index.js</a></span> file to populate that. The code includes a function that uses the promise mechanism to add the response count to the template scope variable, and a helper function to call the plugin service function, <span style="font-family: Courier New, Courier, monospace;">$mmaModQuestionnaire.getUserResponses</span> located in <span style="font-family: Courier New, Courier, monospace;">services/questionnaire.js</span>.<br />
<br />
In the <span style="font-family: "Courier New", Courier, monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART4_FINAL/addons/services/questionnaire.js#L119">services/questionnaire.js</a></span> file, I add the code that will call the new web service I created in the Moodle plugin. This is the part that will actually call the site service and extract the response information. The actual call to the web service is in this line:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">return $mmSite.read('mod_questionnaire_get_user_responses', params, preSets).then(function(response) {</span></blockquote>
This should be all I need to change the behaviour of the questionnaire module in the mobile app, so that it will also show me the number of responses.<br />
<br />
With my new code added to the mobile plugin, and the new services added to the Moodle module, I need to verify that I can still access the questionnaire in the mobile app, and that I can see the number of responses.<br />
<br />
I ramp up my mobile app, click the questionnaire instance, and achieve success:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVO99xdn-ing-qFL3Za36uYh1wi8Q78B32pv0fA1jApUFxaLHnRj8iac4OyGGET9RU2h-y74O7IQIZhJtb4TXKg9IvWGaTPXp3A9EPIo-YMyv63FvUdUpYBeytn1ST-6eQKHobnbQYWfM/s1600/Screen+Shot+2017-04-21+at+3.26.25+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVO99xdn-ing-qFL3Za36uYh1wi8Q78B32pv0fA1jApUFxaLHnRj8iac4OyGGET9RU2h-y74O7IQIZhJtb4TXKg9IvWGaTPXp3A9EPIo-YMyv63FvUdUpYBeytn1ST-6eQKHobnbQYWfM/s400/Screen+Shot+2017-04-21+at+3.26.25+PM.png" width="400" /></a></div>
<br />
I still have a lot of work ahead of me to make my questionnaire plugin functional in the mobile app, but I at least now have a good working understanding of what needs to be done, how to use web services, and how to make them useful in the mobile plugin. I will get back to this (eventually), and document my learnings in future posts.<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com1tag:blogger.com,1999:blog-7094986855457289958.post-90324673895285557082017-03-31T12:15:00.000-04:002017-03-31T12:15:23.501-04:00Add mobile support to your Moodle plugin - part three<h3>
Part Three - Starting to Build the Moodle Mobile Add-on</h3>
In the previous part of this series, I created just enough code to see an impact on the mobile app. Now, I want to begin to actually do things that will lead me to a solution.<br />
<br />
To start with, I decided to take an introductory lesson in AngularJS. If you are not familiar, I highly recommend <a href="http://campus.codeschool.com/courses/shaping-up-with-angular-js/" target="_blank">Code School's Shaping up with AngularJS</a>. This helped me to understand a lot of the javascript I am looking at with other mobile add-ons.<br />
<br />
Learning this little bit, has helped me to understand the code I am looking at, and helps me to make good guesses about what is going on with the Moodle specific code.<br />
<br />
I'm going to use some existing plugins as a starting point for the code I need. Specifically, the <a href="https://github.com/jleyva/moodlemobile2/blob/MOBILE-1586/www/addons/mod/certificate" target="_blank">certificate module</a>, which is a third party activity plugin, and the <a href="https://github.com/crazyserver/moodlemobile2/tree/MOBILE-2002/www/addons/mod/feedback" target="_blank">feedback module</a>, which will be included in core.<br />
<br />
The <span style="font-family: "courier new" , "courier" , monospace;">main.js</span> file defines the necessary parts to the mobile app. The <span style="font-family: "courier new" , "courier" , monospace;">stateProvider</span> configuration sets up the module for displaying when it is clicked in the course. In the <a href="https://github.com/jleyva/moodlemobile2/blob/MOBILE-1586/www/addons/mod/certificate/main.js#L31" target="_blank">certificate code</a>, you can see a controller and a template defined to do this:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">controller: 'mmaModCertificateIndexCtrl',</span><span style="font-family: "courier new" , "courier" , monospace;">templateUrl: 'addons/mod/certificate/templates/index.html'</span></blockquote>
In AngularJS, this indicates that the display setup code will be in <span style="font-family: "courier new" , "courier" , monospace;">controllers/index.js</span> and that will use <span style="font-family: "courier new" , "courier" , monospace;">templates/index.html</span> to display that code. When a user clicks on a questionnaire instance, these will be the main pieces to determine what is displayed.<br />
<br />
The other part of the main.js file, registers the various handlers needed by the mobile app. In the <a href="https://github.com/jleyva/moodlemobile2/blob/MOBILE-1586/www/addons/mod/certificate/main.js#L39" target="_blank">certificate example</a>, you can see there are two: a "course delegate provider" and a "links delegate provider". The course delegate provider is the part responsible to provide functionality for the module to that mobile app. Its job is to handle a click, and use the module plugin code. Without this, the mobile app will display the "not available" message. The links delegate provider provides function for other links to a module instance, not from the main course display. For example, if a link is put into a forum post. Without this, a link will simply launch a web link in the default browser.<br />
<br />
For my module, I'm going to focus on the course delegate provider first. And I will use the index files to affect what is displayed in the mobile app.<br />
<br />
For this part of the discussion, I have posted my files <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/MB32_PART3_BASE/addons" target="_blank">here</a>. This is the minimum I have determined I need to have my plugin work in the mobile app without any errors.<br />
<br />
To start with, I have created my <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_BASE/addons/main.js">main.js</a></span> file to define that the questionnaire plugin should be available, that it uses an index controller and that it has a course content handler. I have taken what I started in the last post, and extended out to provide what is absolutely required.<br />
<br />
Next, I will further flesh out my <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_BASE/addons/services/handlers.js" target="_blank">services/handlers.js</a></span> file. In the first iteration, I only provided an <span style="font-family: "courier new" , "courier" , monospace;">isEnabled</span> function, and it simply returned <span style="font-family: "courier new" , "courier" , monospace;">true</span>. Now I have improved that to call a specific function provided by my questionnaire handler:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">self.isEnabled = function() {<br /> return $mmaModQuestionnaire.isPluginEnabled();<br />};</span></blockquote>
That function will eventually call web services from the Moodle site to verify that the module is enabled.<br />
<br />
I have also added a <span style="font-family: Courier New, Courier, monospace;">getController</span> function, which is required by the framework to provide information for displaying and launching my plugin code, as follows:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">self.getController = function(module, courseid) {<br /> return function($scope) {<br /> $scope.title = module.name;<br /> $scope.icon = 'addons/mod/questionnaire/icon.svg'<br /> $scope.class = 'mma-mod_questionnaire-handler';<br /> $scope.action = function() {<br /> $state.go('site.mod_questionnaire', {module: module, courseid: courseid});<br /> };<br /> };<br />};</span></blockquote>
This code tells the framework that my main handler class will be located in <span style="font-family: "courier new" , "courier" , monospace;">services/questionnaire.js</span> and will be called <span style="font-family: "courier new" , "courier" , monospace;">mmaModQuestionnaire</span>. I have also added an icon to be displayed on the course page, and indicated that it will be located in the root of my plugin code. I copied that icon from the main questionnaire plugin. When I run the mobile app with this code, I should now see the questionnaire icon, rather than the default icon, on the course page.<br />
<br />
The changes I have made here, mean that I need to create a <span style="font-family: "courier new" , "courier" , monospace;">services/questionnaire.js</span> file, with an <span style="font-family: "courier new" , "courier" , monospace;">mmaModQuestionnaire</span> class, that provides an <span style="font-family: "courier new" , "courier" , monospace;">isPluginEnabled</span> method. You can see that file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_BASE/addons/services/questionnaire.js" target="_blank">here</a>. For now, I am keeping the function simple:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">self.isPluginEnabled = function(siteId) {<br /> siteId = siteId || $mmSite.getId();<br /> return Promise.resolve(true);<br />};</span></blockquote>
The function returns a <a href="https://www.promisejs.org/" target="_blank">Javascript Promise</a> <i>(if, like me, you aren't familiar with that, <a href="https://developers.google.com/web/fundamentals/getting-started/primers/promises" target="_blank">this</a> helped me)</i>. Essentially, somewhere in the framework, there is code that will call <span style="font-family: "courier new" , "courier" , monospace;">isEnabled</span> in my course handler and will expect a <span style="font-family: "courier new" , "courier" , monospace;">Promise</span> construct. Since I have not written the necessary web services on the other end to verify that, I am simply going to return a resolved promise to indicate my questionnaire plugin should work.<br />
<br />
The last parts I need to create are the index display handlers I described in the <span style="font-family: "courier new" , "courier" , monospace;">main.js</span> file. I have created the <span style="font-family: "courier new" , "courier" , monospace;">controllers/index.js</span> file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_BASE/addons/controllers/index.js" target="_blank">here</a>, and the <span style="font-family: "courier new" , "courier" , monospace;">templates/index.html</span> file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART3_BASE/addons/templates/index.html" target="_blank">here</a>.<br />
As I mentioned previously, these files work together to define and display what is output when a user clicks on a questionnaire instance in the mobile app. For now, I will set it up to simple display "Hello world!".<br />
<br />
The key elements in <span style="font-family: "courier new" , "courier" , monospace;">index.js</span> are the properties of the <span style="font-family: "courier new" , "courier" , monospace;">$scope</span> variable:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">$scope.title = module.name;<br />$scope.description = module.description;<br />$scope.courseid = courseId;<br />$scope.mymessage = "Hello World!";</span></blockquote>
These are the bits that I can use as variables in the <span style="font-family: "courier new" , "courier" , monospace;">index.html</span> file to display specific information. In Angular, this means parts contained in "{{}}" or defined as tag elements. The template I create looks like this:<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;"><ion-view><br /> <ion-nav-title>{{ title }}</ion-nav-title><br /> <ion-content padding="true" mm-state-class><br /> <mm-course-mod-description description="description"></mm-course-mod-description><br /> {{mymessage}}<br /> </ion-content><br /></ion-view></span></blockquote>
The <span style="font-family: "courier new" , "courier" , monospace;">{{ title }}</span>, <span style="font-family: "courier new" , "courier" , monospace;"><mm-course-mod-description description="description"></span> and the <span style="font-family: "courier new" , "courier" , monospace;">{{mymessage}}</span> elements should be replaced by the corresponding <span style="font-family: "courier new" , "courier" , monospace;">$scope</span> element.<br />
<br />
All of this should be enough for me to reload my mobile app questionnaire plugin code into the mobile app, run it and when I click on a questionnaire link, see a page that says "Hello world!". So I copy my code into the app <span style="font-family: "courier new" , "courier" , monospace;">www/addons/mod/questionnaire</span> directory, run the server and check out my app.<br />
<br />
The course page now displays this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdaARDUJHCQYJk_OLxixhYY9DNfIGK-0M5lKNqS__IHwW4L11KUF8an1UG1-SkdnBNOjIy4WQ8hGdbwVzuh6Qm8kIyLSnU0nTmXXTr0ZvE1IRGK9130wuyQDEGAtA1cGWWgI_ABDY0xWA/s1600/Screen+Shot+2017-03-31+at+11.52.53+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="135" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdaARDUJHCQYJk_OLxixhYY9DNfIGK-0M5lKNqS__IHwW4L11KUF8an1UG1-SkdnBNOjIy4WQ8hGdbwVzuh6Qm8kIyLSnU0nTmXXTr0ZvE1IRGK9130wuyQDEGAtA1cGWWgI_ABDY0xWA/s400/Screen+Shot+2017-03-31+at+11.52.53+AM.png" width="400" /></a></div>
<br />
<br />
Note, that my questionnaire icon is now displaying, rather than the default puzzle piece. Clicking on the questionnaire instance, shows me this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiM9gE_nntLVv6BtJyLK6cV-Lk2mgsDa3pI4zQf8ant4Qpm0MWtJvUt-H_FVcWGOA4TQPMQf42vDFceJ43UbWUc7j9HHIUfDGhIdi1N_-I5YBGQSTdJt-oVAYDoYKoiTWsB4HtOsolWNC4/s1600/Screen+Shot+2017-03-31+at+11.54.48+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="88" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiM9gE_nntLVv6BtJyLK6cV-Lk2mgsDa3pI4zQf8ant4Qpm0MWtJvUt-H_FVcWGOA4TQPMQf42vDFceJ43UbWUc7j9HHIUfDGhIdi1N_-I5YBGQSTdJt-oVAYDoYKoiTWsB4HtOsolWNC4/s400/Screen+Shot+2017-03-31+at+11.54.48+AM.png" width="400" /></a></div>
<br />
Which shows me the title and my "Hello World!" message. Looks like I have succeeded.<br />
<br />
I'm going to leave it there for this post. In my next post, I will build some services to interact with, and build out the link delegate handler as well.<br />
<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-63035657195787659982017-03-21T15:18:00.001-04:002017-03-31T12:18:27.267-04:00Add mobile support to your Moodle plugin - part two<h3>
Part Two - Exploring the Moodle Mobile Add-on</h3>
<div>
In the <a href="http://tandl.churchward.ca/2017/03/add-mobile-support-to-your-moodle.html" target="_blank">first part of this series</a>, I set up a development environment on my local machine. I am going to use this same environment to begin coding the mobile add-on portion of my Moodle questionnaire plug-in.</div>
<div>
<br /></div>
<div>
The <a href="https://docs.moodle.org/dev/Moodle_Mobile_Remote_add-ons#How_to_create_a_remote_add-on" target="_blank">documentation</a> says I need to develop the mobile side as a standard Moodle Mobile add-on and then package it as a remote add-on, with four steps:<br />
<ol>
<li>Develop the required Moodle Web Services</li>
<li>Develop a standard Moodle Mobile add-on</li>
<li>Package the Moodle Mobile add-on as a remote add-on</li>
<li>Include the remote add-on in your Moodle plugin</li>
</ol>
</div>
<div>
The first two steps are where my work will start. The second two steps are what I will need when I am ready to distribute what I have developed for anyone to use in their mobile apps with their Moodle sites. I am also going to start with step two, as this will help me understand what is needed in the mobile portion and what is needed from the Moodle plugin portion.</div>
<br />
Recall, that when I try to access my plugin in the mobile app now, I get the screen:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyzOcg6veV_idBJ-z43-ZyQ6mFgJk3q4ZUTZKeZp0eQdE72gtBSUk5998-cHetdDtk2_-dCu2lXAvbffRaWcPnK9BtejCPadfqMY69ReqaSJXS5ajnQiy1hXGle5f8Bl31pDkDLtWZUFk/s1600/Screen+Shot+2017-02-27+at+4.49.25+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyzOcg6veV_idBJ-z43-ZyQ6mFgJk3q4ZUTZKeZp0eQdE72gtBSUk5998-cHetdDtk2_-dCu2lXAvbffRaWcPnK9BtejCPadfqMY69ReqaSJXS5ajnQiy1hXGle5f8Bl31pDkDLtWZUFk/s320/Screen+Shot+2017-02-27+at+4.49.25+PM.png" width="320" /></a></div>
<br />
My goal, for the start, is to provide enough code so that screen changes.<br />
<br />
To start, I look at the current structure of the mobile app on my local server. Within it is a <span style="font-family: "courier new" , "courier" , monospace;">www/addons</span> directory that contains all of the core mobile addons. I can see, that like a Moodle site, there is a <span style="font-family: "courier new" , "courier" , monospace;">mod</span> subdirectory, and within that, a number of <a href="https://github.com/mchurchward/moodlemobile2/tree/MB32_PART2_INITIAL/www/addons/mod" target="_blank">subdirectories</a> corresponding to each of the supported core activity plugins.<br />
<br />
For ease of development, I am going to add my plugin to the same location. That way, I can update my code within the mobile app, and access it with the browser, to see my work. Essentially, my local files will stand-in as the mobile app, and the questionnaire mobile addon will act as a core activity plugin.<br />
<br />
For purposes of code storage, I am going to add an <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/MB32_PART2_INITIAL" target="_blank">addons subdirectory</a> to my questionnaire repository and add the code that will go into the mobile addon there. This code will not be required, nor run, on a Moodle site, but I want to keep it together with my Moodle plugin for development ease.<br />
<br />
Through a <a href="https://moodle.org/mod/forum/discuss.php?d=348597" target="_blank">productive discussion</a> with the mobile development team, I have determined that getting my add-on registered with the mobile app requires code that registers a content handler. In the add-on structure, this happens in the <span style="font-family: "courier new" , "courier" , monospace;">main.js</span> file in a <span style="font-family: "courier new" , "courier" , monospace;">config</span> statement (<a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART2_INITIAL/addons/main.js" target="_blank">full code here</a>):<br />
<div>
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">.config(function($mmCourseDelegateProvider) {<br /> $mmCourseDelegateProvider.registerContentHandler('mmaModQuestionnaire', 'questionnaire', '$mmaModQuestionnaireHandlers.courseContent');<br />});</span></blockquote>
Essentially, the mobile framework translates this code such that it knows there must be a <span style="font-family: "courier new" , "courier" , monospace;">services</span> subdirectory and within that a file named <span style="font-family: "courier new" , "courier" , monospace;">handlers.js</span> that contains a <span style="font-family: "courier new" , "courier" , monospace;">courseContent</span> function. I create that structure with a very simple <span style="font-family: "courier new" , "courier" , monospace;">courseContent</span> function as follows (<a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/MB32_PART2_INITIAL/addons/services/handlers.js" target="_blank">full code here</a>):<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">self.courseContent = function() {<br /> var self = {};<br /> /**<br /> * Whether or not the module is enabled for the site.<br /> *<br /> * @return {Boolean}<br /> */<br /> self.isEnabled = function() {<br /> return true;<br /> };<br /> return self;<br />};</span></blockquote>
In conversations with the developers, I have determined that I need an <span style="font-family: "courier new" , "courier" , monospace;">isEnabled</span> function, that returns true if the plugin is enabled, for my addon to be acknowledged and not display the "<i>plugin that is not yet supported</i>" message. For now, I just send true with no logic confirming that. Eventually, this will call a web service from the actual questionnaire plugin on the Moodle site to determine this. I also know that when I run my local version of the mobile app using the ionic server, it will build my addons code into the app's main code file, <span style="font-family: "courier new" , "courier" , monospace;">www/build/mm.bundle.js</span>.<br />
<br />
After dropping these two files into my mobile app's <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodlemobile2/tree/MB32_PART2_FIRSTADDON/www/addons/mod/questionnaire" target="_blank">www/addons/mod/questionnaire</a></span> directory, I run the ionic server (removed some messages for brevity):<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">ionic serve --browser chromium<br />Running 'serve:before' gulp task before serve<br />[13:43:32] Starting 'build'...<br />[13:43:32] Starting 'sass-build'...<br />[13:43:32] Starting 'lang'...<br />[13:43:32] Starting 'watch'...<br />[13:43:41] Finished 'watch' after 9.04 s<br />[13:43:47] Finished 'sass-build' after 15 s<br />[13:43:47] Starting 'sass'...<br />[13:43:48] Finished 'lang' after 15 s<br />[13:43:49] Finished 'sass' after 1.92 s<br />[13:43:50] Finished 'build' after 18 s<br />[13:43:50] Starting 'config'...<br />[13:43:50] Finished 'config' after 17 ms<br />[13:43:50] Starting 'default'...<br />[13:43:50] Finished 'default' after 6.02 μs<br />[13:43:50] Starting 'serve:before'...<br />[13:43:50] Finished 'serve:before' after 3.15 μs<br />Running live reload server: http://localhost:35729<br />Watching: www/**/*.html, www/build/**/*, www/index.html, !www/lib/**/*<br />√ Running dev server: http://localhost:8100<br />Ionic server commands, enter:<br /> restart or r to restart the client app from the root<br /> goto or g and a url to have the app navigate to the given url<br /> consolelogs or c to enable/disable console log output<br /> serverlogs or s to enable/disable server log output<br /> quit or q to shutdown the server and exit<br />ionic $</span></blockquote>
I can see a lot of build activity happening. I then point my browser to the server instance and see a lot more activity kick in. When I access my course, and then click on my questionnaire activity, I no longer get the "plugin that is not yet supported" message:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVIWwFGfZlvK5j2ZWRGhnR2oVwakZtANM55M877iSk3yI7y9F5PONQqb7kth2dE7SKEz6UWlEp8YLZkxg1AVkCGw79sVaoqYNuVFnh3Yfq95CT6q8QpgASDCvzZEJHR_VkZtijDXyf3Js/s1600/Screen+Shot+2017-03-21+at+1.58.51+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="104" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVIWwFGfZlvK5j2ZWRGhnR2oVwakZtANM55M877iSk3yI7y9F5PONQqb7kth2dE7SKEz6UWlEp8YLZkxg1AVkCGw79sVaoqYNuVFnh3Yfq95CT6q8QpgASDCvzZEJHR_VkZtijDXyf3Js/s320/Screen+Shot+2017-03-21+at+1.58.51+PM.png" width="320" /></a></div>
<br />
I don't get the questionnaire displayed, but I didn't expect to. I have at least learned how to impact the mobile app with my addon.<br />
<br />
I also take a look at the <span style="font-family: "courier new" , "courier" , monospace;">www/build/mm.bundle.js</span> to see if it now contains the code I created for the questionnaire addon. A quick search of the code reveals that it does. So I have confirmed that the mobile app has successfully picked up the questionnaire addon code and added it to the app.<br />
<br />
In the <a href="http://tandl.churchward.ca/2017/03/add-mobile-support-to-your-moodle_31.html" target="_blank">next post</a>, I will flesh out more of the mobile add-on code, and begin to look what the web services needed on the questionnaire plugin side.<br />
<br /></div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-33928718530505307672017-03-03T10:37:00.000-05:002017-03-21T15:20:08.621-04:00Add mobile support to your Moodle plugin - part one<h3>
Part One - Setting up a Development Environment</h3>
With version 3 of Moodle came the Moodle Mobile app. Out of the box, most of the core Moodle plugins came supported. But, third-party plugins do not function by default in the mobile app.<br />
<br />
In Moodle 3.1, the mobile app added support for "<a href="https://docs.moodle.org/dev/Moodle_Mobile_Remote_add-ons" target="_blank">Remote add-ons</a>". This support allows plugin providers to add support so that their plugin can function within the mobile app.<br />
<br />
Now, I should point out that Moodle is currently working on a <a href="https://docs.google.com/document/d/1Lnau3wPbgzzV7ekx_cnWOc8vzm_0kaw2Ip0pWgMZC2U" target="_blank">much simpler system for adding mobile support to your plugin</a>, but it will be a while before it is ready. When it is ready, the work I am about to undertake will likely be moot.<br />
<br />
This post will be the first in a series where I will attempt to add mobile support for my questionnaire module. I say "attempt", because this will be using some technologies that I am not familiar with so I will be learning as I go. I won't guarantee success, but I will document the efforts.<br />
<br />
To start with, the main developer documents for Moodle Mobile are <a href="https://docs.moodle.org/dev/Category:Mobile" target="_blank">here</a>. Looking through that list, I'm going to start with <a href="https://docs.moodle.org/dev/Setting_up_your_development_environment_for_Moodle_Mobile_2" target="_blank">setting up a development environment</a> on my Mac.<br />
<br />
I already have Chrome installed on my Mac, so I can skip that step. The next step it says is to install "Node.js". Keep in mind, I don't really know what I am doing here (yet), so I'm not sure how this comes into play, but I'm going to follow instructions. :-)<br />
<br />
The documentation suggest using Macports to install Nodejs. It also says I need to use version "v0.12.7". But, it looks like Macports only has versions from 4.* to 7.*. So, Macports is out. The documentation also provides a <a href="https://nodejs.org/dist/v0.12.7/" target="_blank">direct download link</a> for "v0.12.7", and it has a Mac "pkg". So I download that and run the Mac installer. As a result, I have "node" installed at <span style="font-family: "courier new" , "courier" , monospace;">/usr/local/bin/node</span> and "npm" installed at <span style="font-family: "courier new" , "courier" , monospace;">/usr/local/bin/npm</span>.<br />
<br />
The next step asks me to run the command <span style="font-family: "courier new" , "courier" , monospace;">npm cache clean</span>. When I do this, I get a series of errors. which seem to indicate permission problems. Likely I need to elevate the permission level this command runs at, so I add a <span style="font-family: "courier new" , "courier" , monospace;">sudo</span> to the front of the command and try again. The new command, <span style="font-family: "courier new" , "courier" , monospace;">sudo npm cache clean</span> works fine. I'm going to assume I will need <span style="font-family: "courier new" , "courier" , monospace;">sudo</span> for the rest of the commands in the document as well.<br />
<br />
Next, I am asked to install "<a href="https://ionicframework.com/" target="_blank">ionic</a>". I excute the command:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">sudo npm install -g cordova ionic</span></blockquote>
I get some issues displayed:<br />
<blockquote class="tr_bq">
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Times; color: #000000; -webkit-text-stroke: #000000}
p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Times; color: #000000; -webkit-text-stroke: #000000; min-height: 14.0px}
span.s1 {font-kerning: none}
span.s2 {font: 12.0px 'Songti SC'; font-kerning: none}
</style>
<br />
<div class="p1">
<span class="s1">npm WARN engine cordova@6.5.0: wanted: {"node":">=4.0.0"} (current: {"node":"0.12.7","npm":"2.11.3"})</span></div>
<div class="p1">
<span class="s1">npm WARN engine request@2.79.0: wanted: {"node":">= 4"} (current: {"node":"0.12.7","npm":"2.11.3"})</span></div>
<div class="p1">
<span class="s1">npm WARN deprecated node-uuid@1.4.7: use uuid module instead</span></div>
<div class="p1">
<span class="s1">/usr/local/bin/cordova -> /usr/local/lib/node_modules/cordova/bin/cordova</span></div>
<div class="p1">
<span class="s1">/usr/local/bin/ionic -> /usr/local/lib/node_modules/ionic/bin/ionic</span></div>
<div class="p1">
<span class="s1">cordova@6.5.0 /usr/local/lib/node_modules/cordova</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> underscore@1.7.0</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> q@1.0.1</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> nopt@3.0.1 (abbrev@1.1.0)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> update-notifier@0.5.0 (is-npm@1.0.0, semver-diff@2.1.0, chalk@1.1.3, string-length@1.0.1, repeating@1.1.3, configstore@1.4.0, latest-version@1.0.1)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> insight@0.8.4 (object-assign@4.1.1, uuid@3.0.1, lodash.debounce@3.1.1, async@1.5.2, chalk@1.1.3, configstore@1.4.0, os-name@1.0.3, tough-cookie@2.3.2, request@2.79.0, inquirer@0.10.1)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> cordova-common@2.0.0 (cordova-registry-mapper@1.1.15, ansi@0.3.1, semver@5.3.0, osenv@0.1.4, underscore@1.8.3, q@1.4.1, unorm@1.4.1, shelljs@0.5.3, minimatch@3.0.3, glob@5.0.15, bplist-parser@0.1.1, elementtree@0.1.7, plist@1.2.0)</span></div>
<div class="p1">
<span class="s2">└──</span><span class="s1"> cordova-lib@6.5.0 (valid-identifier@0.0.1, opener@1.4.1, cordova-registry-mapper@1.1.15, properties-parser@0.2.3, nopt@3.0.6, unorm@1.3.3, shelljs@0.3.0, semver@4.3.6, glob@5.0.15, dep-graph@1.1.0, xcode@0.9.1, elementtree@0.1.6, init-package-json@1.9.4, cordova-serve@1.0.1, request@2.47.0, tar@1.0.2, cordova-fetch@1.0.2, aliasify@1.9.0, plist@1.2.0, cordova-js@4.2.1, cordova-create@1.0.2, npm@2.15.11)</span></div>
<div class="p2">
<span class="s1"></span><br /></div>
<div class="p1">
<span class="s1">ionic@2.2.1 /usr/local/lib/node_modules/ionic</span></div>
<div class="p1">
<span class="s2">└──</span><span class="s1"> @ionic/app-generators@0.0.3</span></div>
</blockquote>
But they seem to be warnings about outdated "node". I know we are using an older version of "node", so I believe this will not be an issue.<br />
<br />
Now I need to install the "<a href="https://bower.io/" target="_blank">bower</a>" and "<a href="http://gulpjs.com/" target="_blank">gulp</a>" node packages.<br />
<br />
Bower:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">sudo npm install -g bower<br />/usr/local/bin/bower -> /usr/local/lib/node_modules/bower/bin/bower<br />bower@1.8.0 /usr/local/lib/node_modules/bower</span></blockquote>
Gulp:<br />
<blockquote class="tr_bq">
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Times; color: #000000; -webkit-text-stroke: #000000}
span.s1 {font-kerning: none}
span.s2 {font: 12.0px 'Songti SC'; font-kerning: none}
</style>
<br />
<div class="p1">
<span class="s1">sudo npm install -g gulp</span></div>
<div class="p1">
<span class="s1">npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue</span></div>
<div class="p1">
<span class="s1">npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue</span></div>
<div class="p1">
<span class="s1">npm WARN deprecated graceful-fs@1.2.3: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to graceful-fs@^4.0.0 as soon as possible. Use 'npm ls graceful-fs' to find it in the tree.</span></div>
<div class="p1">
<span class="s1">/usr/local/bin/gulp -> /usr/local/lib/node_modules/gulp/bin/gulp.js</span></div>
<div class="p1">
<span class="s1">gulp@3.9.1 /usr/local/lib/node_modules/gulp</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> interpret@1.0.1</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> pretty-hrtime@1.0.3</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> deprecated@0.0.1</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> archy@1.0.0</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> tildify@1.2.0 (os-homedir@1.0.2)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> minimist@1.2.0</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> v8flags@2.0.11 (user-home@1.1.1)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> chalk@1.1.3 (escape-string-regexp@1.0.5, supports-color@2.0.0, ansi-styles@2.2.1, has-ansi@2.0.0, strip-ansi@3.0.1)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> semver@4.3.6</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> orchestrator@0.3.8 (sequencify@0.0.7, stream-consume@0.1.0, end-of-stream@0.1.5)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> gulp-util@3.0.8 (object-assign@3.0.0, array-differ@1.0.0, lodash._reescape@3.0.0, lodash._reinterpolate@3.0.0, lodash._reevaluate@3.0.0, beeper@1.1.1, array-uniq@1.0.3, replace-ext@0.0.1, dateformat@2.0.0, has-gulplog@0.1.0, fancy-log@1.3.0, vinyl@0.5.3, gulplog@1.0.0, lodash.template@3.6.2, through2@2.0.3, multipipe@0.1.2)</span></div>
<div class="p1">
<span class="s2">├──</span><span class="s1"> liftoff@2.3.0 (lodash.isstring@4.0.1, lodash.isplainobject@4.0.6, rechoir@0.6.2, extend@3.0.0, flagged-respawn@0.3.2, lodash.mapvalues@4.6.0, resolve@1.3.2, fined@1.0.2, findup-sync@0.4.3)</span></div>
<div class="p1">
<span class="s2">└──</span><span class="s1"> vinyl-fs@0.3.14 (strip-bom@1.0.0, graceful-fs@3.0.11, vinyl@0.4.6, defaults@1.0.3, mkdirp@0.5.1, through2@0.6.5, glob-stream@3.1.18, glob-watcher@0.0.6)</span></div>
</blockquote>
Again, there are some warnings about outdated releases. But I'm just going to leave them as is for now.<br />
<br />
The next step talks about "Push notifications for Mac", and refers me to "https://cocoapods.org/" and the Moodle tracker item <a href="https://tracker.moodle.org/browse/MOBILE-1970" target="_blank">MOBILE-1970</a>. It looks like this will resolve a problem I <i>may</i> have later on when running iOS versions of the app. So, I execute the command:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">sudo gem install cocoapods</span></blockquote>
At the end of that, I have 27 new gems installed in <span style="font-family: "courier new" , "courier" , monospace;">/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/</span>.<br />
<br />
I already have my git clone of the <a href="https://github.com/moodlehq/moodlemobile2.git" target="_blank">moodlemobile2 repository</a>, so I set my current directory to it. From there, I execute:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">sudo npm install</span></blockquote>
There are a number of warnings and errors that are output as this executes. Most of them appear to be problems with outdated modules. I am going to disregard those for now.<br />
<br />
Now I run the following three commands, one after the other:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">sudo ionic platform add android@5.1.1<br />sudo ionic platform add ios@4.1.0<br />sudo ionic state restore</span></blockquote>
<div>
All three seem to install okay. The only warnings I receive are for outdated dependencies.</div>
<div>
<br /></div>
<div>
So, I execute:</div>
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">sudo bower install</span></blockquote>
<div>
And I get:</div>
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;"><span class="s1">bower </span><span class="s2">ESUDO </span><span class="s1"> Cannot be run with sudo</span></span></blockquote>
<div>
<span class="s1">So, guess I won't be using <span style="font-family: "courier new" , "courier" , monospace;">sudo</span> here.</span></div>
<div>
<span class="s1"><br /></span></div>
<div>
<span class="s1">I rerun the command without <span style="font-family: "courier new" , "courier" , monospace;">sudo</span>, and get a long list of files being installed. A</span>ll of them went into the local repo below <span style="font-family: "courier new" , "courier" , monospace;">www/lib/</span>.</div>
<div>
<br /></div>
<div>
So, gulp:<br />
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Courier; color: #000000; -webkit-text-stroke: #000000}
p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px 'Courier New'; color: #000000; -webkit-text-stroke: #000000}
span.s1 {font-kerning: none}
</style>
<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">gulp<br />[16:30:43] Using gulpfile ~/www/moodlemobile2/gulpfile.js<br />[16:30:43] Starting 'build'...<br />[16:30:43] Starting 'sass-build'...<br />[16:30:43] Starting 'lang'...<br />[16:30:48] Finished 'sass-build' after 5.38 s<br />[16:30:48] Starting 'sass'...<br />[16:30:49] Finished 'lang' after 5.59 s<br />[16:30:50] Finished 'sass' after 1.7 s<br />[16:30:50] Finished 'build' after 7.46 s<br />[16:30:50] Starting 'config'...<br />[16:30:51] Finished 'config' after 16 ms<br />[16:30:51] Starting 'default'...<br />[16:30:51] Finished 'default' after 5.49 μs</span></blockquote>
Which again added files below my local repo.</div>
<div>
<br /></div>
<div>
At this point, I think I have everything ready to be able to access the Moodle Mobile app from my local development environment. The links tell me to start Chrome using:</div>
<div>
<pre></pre>
<pre> open -a "Google Chrome" --args --allow-file-access-from-files --disable-web-security --user-data-dir</pre>
<pre></pre>
<br />
That seems to just move my Chrome window to the front. Probably because I already had it open? I'll assume it's all good.<br />
<br />
Now, to run the ionic server:<br />
<blockquote class="tr_bq">
<span style="font-family: "courier new" , "courier" , monospace;">ionic serve --browser chromium<br />******************************************************<br /> Dependency warning - for the CLI to run correctly, <br /> it is highly recommended to install/upgrade the following: <br /> Please install your Cordova CLI to version >=4.2.0 `npm install -g cordova`<br /> Install ios-sim to deploy iOS applications.`npm install -g ios-sim` (may require sudo)<br /> Install ios-deploy to deploy iOS applications to devices. `npm install -g ios-deploy` (may require sudo)<br />******************************************************<br />WARN: ionic.project has been renamed to ionic.config.json, please rename it.<br />WARN: ionic.project has been renamed to ionic.config.json, please rename it.<br />Multiple addresses available.<br />Please select which address to use by entering its number from the list below:<br /> 1) 10.120.211.137 (en4)<br /> 2) 10.120.211.40 (en0)<br /> 3) localhost<br />Address Selection: 3<br />Selected address: localhost<br />Running live reload server: http://localhost:35729<br />Watching: www/**/*, !www/lib/**/*<br />√ Running dev server: http://localhost:8100<br />Ionic server commands, enter:<br /> restart or r to restart the client app from the root<br /> goto or g and a url to have the app navigate to the given url<br /> consolelogs or c to enable/disable console log output<br /> serverlogs or s to enable/disable server log output<br /> quit or q to shutdown the server and exit</span></blockquote>
Since I chose the 'localhost' option I enter 'http://localhost:8100' in my chrome browser window. The browser displays the Moodle Mobile app! I configure it to my local running copy of Moodle, and I'm in business!<br />
<br />
Using the simulated mobile browser, I navigate to my test course:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaz5SDBh8rlAxW2zZj4LwTu_KlftN-ZBQSdzgKspYkWENP0UACLLDN9T5FnG5TQCUVYURrBLWIBeFdSpYKBED9sTtGO1JHGv_JsNF-jalSn6tgs7bo98JtDQPgeKvlwJYrsq4gZ-99mco/s1600/Screen+Shot+2017-02-27+at+4.49.13+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="247" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaz5SDBh8rlAxW2zZj4LwTu_KlftN-ZBQSdzgKspYkWENP0UACLLDN9T5FnG5TQCUVYURrBLWIBeFdSpYKBED9sTtGO1JHGv_JsNF-jalSn6tgs7bo98JtDQPgeKvlwJYrsq4gZ-99mco/s320/Screen+Shot+2017-02-27+at+4.49.13+PM.png" width="320" /></a></div>
<br />
I can see a "questionnaire" instance, so I select that:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTJVe5SdLfniftAcnq-isILUi6azhVjsfn2MSGXuCAEXgpfYJvF792xx2uvHxcfaTqZlX_WbDiVX2dNE0uqy_ztqDWzRe2P3x24JeVd1oP87ZsYxscX6kT101dMmbHJuncgAuUwchyzBg/s1600/Screen+Shot+2017-02-27+at+4.49.25+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTJVe5SdLfniftAcnq-isILUi6azhVjsfn2MSGXuCAEXgpfYJvF792xx2uvHxcfaTqZlX_WbDiVX2dNE0uqy_ztqDWzRe2P3x24JeVd1oP87ZsYxscX6kT101dMmbHJuncgAuUwchyzBg/s320/Screen+Shot+2017-02-27+at+4.49.25+PM.png" width="320" /></a></div>
<br />
And, I see that I really do need to add mobile support to the plugin!<br />
<br />
<a href="http://tandl.churchward.ca/2017/03/add-mobile-support-to-your-moodle-plugin.html" target="_blank">Next posts</a> will be me trying to make this happen.<br />
<br /></div>
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo; color: #000000; background-color: #ffffff}
p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo; color: #000000; background-color: #ffffff; min-height: 13.0px}
span.s1 {font-variant-ligatures: no-common-ligatures}
</style><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-81593636713186729662017-02-03T11:31:00.000-05:002017-02-03T11:31:41.731-05:00Adding search to your Moodle plugin - post mortem<h3>
Post Mortem - Production ready search</h3>
I ended up in a lot of discussions with other Moodle developers on the best way to implement the question content search. Originally, I created a separate search area, but was hampered by the fact that I couldn't reliably keep a record per question linked to a questionnaire instance.<br />
<br />
The search API didn't allow me to create my own index other than an integer. If it would have allowed me to construct my own, I could have stored information that identified the questionnaire and question. This resulted in the creation of the Moodle tracker item <a href="https://tracker.moodle.org/browse/MDL-57857" target="_blank">MDL-57857</a>.<br />
<br />
However, as we discussed this particular problem more, it really seemed that if I was searching for questionnaire instances that contained the searched for question content, then the index should really be part of the activity search. This resulted in the <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/SEARCHFINAL/classes/search/activity.php" target="_blank">update</a> I posted at the end of the last post. While that worked, the return results displayed weren't very attractive and, as was pointed out, would also include question content that may never have been displayed to the use. Questionnaires can choose questions to display based on answers to other questions.<br />
<br />
At that point, I realized that the most likely use case for searching for question content would be for users who can access all of the question content of a questionnaire. This would be users who could actually edit the questions and users who could see all of the responses to any question.<br />
<br />
So, it made more sense to again separate out the question search area into its own search area. This allows for two important things:<br />
<br />
<ol>
<li>Allows the administrator of the site to determine if they want to even allow question content searching.</li>
<li>Allows the access for this content to be limited to users who have the ability to see all of the question content for a questionnaire.</li>
</ol>
<div>
You can see the final code I decided on <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/SEARCHPOSTMORTEM/classes/search" target="_blank">here</a>. The question area still stores one record per questionnaire activity, like the activity area, but it uses a different set of access rules. And, since it is a separate area, it can be enabled/disabled globally, and filtered on separately.</div>
<div>
<br /></div>
<div>
I believe that until there is a way to store information in the search index that allows me to go directly to the question within a specific questionnaire, this is the best solution.</div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-85719593728422386782017-02-01T13:28:00.001-05:002017-02-02T16:43:26.492-05:00Adding search to your plugin - Part three<h3>
Part Three - Indexing More of your Plugin</h3>
In the last post, I added more searchable content to my plugin's search indexing function, so that additional information fields specific to each instance of a plugin were included. Now I'm going to try and add question content to the search indexing.<br />
<br />
The <a href="https://docs.moodle.org/dev/Search_API#Base_class" target="_blank">documentation</a> tells me that if I'm going to index other activity data, I should use the <span style="font-family: "courier new" , "courier" , monospace;">base_mod</span> class instead of the <span style="font-family: "courier new" , "courier" , monospace;">base_activity</span> class. The base_mod class is located in the core file <span style="font-family: "courier new" , "courier" , monospace;">/search/classes/base_mod.php</span>. Looking at that class, I see that it only contains one method: <span style="font-family: "courier new" , "courier" , monospace;">get_cm</span>. All the rest come from the class it extends, <span style="font-family: "courier new" , "courier" , monospace;">base</span>. The functions I used before, <span style="font-family: "courier new" , "courier" , monospace;">get_recordset_by_timestamp()</span> and <span style="font-family: "courier new" , "courier" , monospace;">get_document()</span> are abstract functions of <span style="font-family: "courier new" , "courier" , monospace;">base</span>. This means I <b>must</b> provide their implementations. I also note that three other abstract functions, <span style="font-family: "courier new" , "courier" , monospace;">check_access()</span>, <span style="font-family: "courier new" , "courier" , monospace;">get_doc_url()</span> and <span style="font-family: "courier new" , "courier" , monospace;">get_context_url()</span> that I will likewise need to provide implementations for. In my previous work, these three functions were provided to me by the <span style="font-family: "courier new" , "courier" , monospace;">base_activity</span> class.<br />
<br />
Back to the documentation, the first step it points out is that if I am going to add a new search area for my plugin, I need to provide a name for it in my language strings. Since I am going to index the questions, I add the string:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> $string['search:question'] = 'Questionnaire - questions';</span><br />
<div>
<br /></div>
<div>
to my <span style="font-family: "courier new" , "courier" , monospace;">lang/en/questionnaire.php</span> file.</div>
<div>
<br /></div>
<div>
Next, I need to provide the indexed content from my questions to the search engine, using the <span style="font-family: "courier new" , "courier" , monospace;">get_recordset_by_timestamp()</span> function. Recall that this function is responsible to construct the necessary database query for all of the content I want available for searching, execute that query, and then return the resulting recordset. For this, I create a new file called '<span style="font-family: "courier new" , "courier" , monospace;">classes/search/question.php</span>' and create a new <span style="font-family: "courier new" , "courier" , monospace;">question</span> class extending the <span style="font-family: "courier new" , "courier" , monospace;">\core_search\base_mod</span> class.</div>
<div>
<br /></div>
<div>
For questionnaire questions, I will need to join two tables. Question data is contained in a table that contains a reference to a survey id. That survey id is also referenced in the main questionnaire table. Since question data is not accessed outside of a survey context, I will need to create SQL that returns each question with an associated questionnaire and survey.</div>
<div>
<br /></div>
<div>
The function I create looks like this (full file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/SEARCHPART3/classes/search/question.php#L39" target="_blank">here</a>):</div>
<div>
<br /></div>
<div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_recordset_by_timestamp($modifiedfrom = 0) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> global $DB;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $sql = 'SELECT qq.id, q.id AS questionnaireid, q.timemodified, ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> 'q.course AS courseid, q.introformat, </span><span style="font-family: "courier new" , "courier" , monospace;">qq.name, qq.content ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> 'FROM {questionnaire} q ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> 'INNER JOIN {questionnaire_question} qq ON q.sid = qq.survey_id AND ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> 'qq.deleted = \'n\' ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> 'WHERE q.timemodified >= ? ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> 'ORDER BY q.timemodified ASC';</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return $DB->get_recordset_sql($sql, [$modifiedfrom]);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
</div>
<div>
<br /></div>
<div>
Note that I use the question id field, '<span style="font-family: "courier new" , "courier" , monospace;">qq.id</span>', as the first field. Moodle requires the first field of a query return to be a unique field that is used as an index for the resulting data array. Since there will be multiple questions per questionnaire and survey record, I can't use those. I will need the questionnaire id field though, so I rename it to '<span style="font-family: "courier new" , "courier" , monospace;">questionnaireid</span>' in the returned data. The question name and content fields, '<span style="font-family: "courier new" , "courier" , monospace;">qq.name</span>' and '<span style="font-family: "courier new" , "courier" , monospace;">qq.content</span>', contain the data that will actually be searched. This function will provide the data that will be indexed by the search function.</div>
<div>
<br /></div>
<div>
Now I need to provide the <span style="font-family: "courier new" , "courier" , monospace;">get_document()</span> function. This is the function that looks at an indexed record returned from a search query and constructs the document object that is displayed on the search results screen. A <span style="font-family: "courier new" , "courier" , monospace;">document</span> object contains a '<span style="font-family: "courier new" , "courier" , monospace;">title</span>' and '<span style="font-family: "courier new" , "courier" , monospace;">content</span>' field. I set the question '<span style="font-family: "courier new" , "courier" , monospace;">name</span>' data to the document '<span style="font-family: "courier new" , "courier" , monospace;">title</span>' and the question '<span style="font-family: "courier new" , "courier" , monospace;">content</span>' data to the document '<span style="font-family: "courier new" , "courier" , monospace;">content</span>'. The rest of the object is pretty much boilerplate from the <span style="font-family: "courier new" , "courier" , monospace;">base_activity</span> class code and the documentation.</div>
<div>
<br /></div>
<div>
The function I create looks like this (full file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/SEARCHPART3/classes/search/question.php#L58" target="_blank">here</a>):</div>
<div>
<br /></div>
<div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_document($record, $options = []) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> try {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $cm = $this->get_cm('questionnaire', $record->questionnaireid,</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $record->courseid);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $context = \context_module::instance($cm->id);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> } catch (\dml_missing_record_exception $ex) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Notify it as we run here as admin, we should see everything.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> debugging('Error retrieving ' . $this->areaid . ' ' . $record->id .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> ' document, not all required data is available: ' .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $ex->getMessage(), DEBUG_DEVELOPER);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return false;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> } catch (\dml_exception $ex) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Notify it as we run here as admin, we should see everything.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> debugging('Error retrieving ' . $this->areaid . ' ' . $record->id .</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return false;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Prepare associative array with data from DB.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc = \core_search\document_factory::instance($record->id, $this->componentname,</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $this->areaname);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('title', content_to_text($record->name, false));</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('content', content_to_text($record->content, $record->introformat));</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('contextid', $context->id);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('courseid', $record->courseid);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('modified', $record->timemodified);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return $doc;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
</div>
<div>
<br /></div>
<div>
Prior to constructing the returned <span style="font-family: "courier new" , "courier" , monospace;">document</span> object, the search API needs to verify that the requesting user has the right permissions to access the Moodle item. This is done with <span style="font-family: "courier new" , "courier" , monospace;">check_access()</span> function. Generally speaking, what this function should do is to extract the Moodle item in the correct Moodle context, and then check that item and context with the requesting user's capabilities. In my case, a question's data is tied to the questionnaire it belongs to. So determining if a user can see that is based on their access to the specific questionnaire instance.</div>
<div>
<br /></div>
<div>
For questionnaire, this is complicated by the fact that a survey (which contains the questions) can belong to more than one questionnaire (in the case of <a href="https://docs.moodle.org/32/en/Adding/editing_a_questionnaire#Content_Options" target="_blank">public questionnaires</a>). While this is an uncommon use, it can still exist. The <span style="font-family: "courier new" , "courier" , monospace;">check_access()</span> function only provides me with the unique <span style="font-family: "courier new" , "courier" , monospace;">id</span> of the data that was saved. For questions, this is the question <span style="font-family: "courier new" , "courier" , monospace;">id</span> field. This means that I do not know which questionnaire instance I need to check when I receive this <span style="font-family: "courier new" , "courier" , monospace;">id</span> data. So my function will need to "guess" at which questionnaire to return by looking for one that contains the question data that is accessible to the user.</div>
<div>
<br /></div>
<div>
The function I create looks like this (full file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/SEARCHPART3/classes/search/question.php#L92" target="_blank">here</a>):</div>
<div>
<br /></div>
<div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> public function check_access($id) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> global $DB;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> try {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Questions are in surveys and surveys can be used in multiple questionnaires.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $question = $DB->get_record('questionnaire_question', ['id' => $id],</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> '*', MUST_EXIST);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $questionnaires = $DB->get_records('questionnaire',</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> ['sid' => $question->survey_id]);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> if (empty($questionnaires)) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return \core_search\manager::ACCESS_DELETED;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $cmsinfo = [];</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> foreach ($questionnaires as $questionnaire) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $cmsinfo[] = $this->get_cm('questionnaire', $questionnaire->id,</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $questionnaire->course);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> } catch (\dml_missing_record_exception $ex) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return \core_search\manager::ACCESS_DELETED;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> } catch (\dml_exception $ex) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return \core_search\manager::ACCESS_DENIED;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Recheck uservisible although it should have already been checked in core_search.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Use the first one that is visible. Otherwise exit.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $cmvisible = false;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> foreach ($cmsinfo as $cminfo) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> if ($cminfo->uservisible !== false) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $cmvisible = $cminfo;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> break;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> if ($cmvisible === false) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return \core_search\manager::ACCESS_DENIED;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $context = \context_module::instance($cminfo->id);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> if (!has_capability('mod/questionnaire:view', $context)) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return \core_search\manager::ACCESS_DENIED;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return \core_search\manager::ACCESS_GRANTED;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
</div>
<div>
<br /></div>
<div>
Without going into too much detail, the basic function is:</div>
<div>
<ul>
<li>find the question data and all questionnaires that might contain it,</li>
<li>get a valid course module for each questionnaire,</li>
<li>find the first course module that is visible to the user and assume this is the one,</li>
<li>check that the user has the proper capabilities to view that questionnaire.</li>
</ul>
<div>
For this post, the functionality will suffice to show how this function works. Unless the function finds reason not to grant access, then granted access is returned.</div>
</div>
<div>
<br /></div>
<div>
<i>(Note - I may be able to modify my functions to return a search id field that I construct to identify both the question and the questionnaire. I will look into this later, as I am not happy with "guessing" which questionnaire is used.)</i></div>
<div>
<i><br /></i></div>
<div>
The last two functions I need to provide are <span style="font-family: "courier new" , "courier" , monospace;">get_doc_url()</span> and <span style="font-family: "courier new" , "courier" , monospace;">get_context_url()</span>. These functions provide the link to the resulting page that will display the found content in its proper Moodle context. Since there is no way to look at only a question instance, my functions will provide a link to the questionnaire. Both functions will provide the same link. I write the code for the <span style="font-family: "courier new" , "courier" , monospace;">get_context_url()</span> and just call that from the <span style="font-family: "courier new" , "courier" , monospace;">get_doc_url()</span> function.</div>
<div>
<br /></div>
<div>
The get_context_url code looks like this (full file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/SEARCHPART3/classes/search/question.php#L140" target="_blank">here</a>):</div>
<div>
<br /></div>
<div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_context_url(\core_search\document $doc) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $context = \context::instance_by_id($doc->get('contextid'));</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return new \moodle_url('/mod/questionnaire/view.php',</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> ['id' => $context->instanceid]);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
</div>
<div>
<br /></div>
<div>
Pretty straightforward.</div>
<div>
<br /></div>
<div>
Since I have everything I need, I load the new code, reindex all of the site contents and give it a test.</div>
<div>
<br /></div>
<div>
One of the first things I notice is that there is a new "search area" in the dropdown called "Questionnaire - questions":</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMdybxfNqt8t_bNLdRlOp-KxgoPNFseAAIpAq7YNJlv05226f_d2OZVldTR87Q2WQ30AyRCF-drZWYFMRzPVSxHKSTLTSCfL9r_MpG6Tn5N9a6JGcMNGfJY_-LdIeA7ltM85UiwMLsEvg/s1600/Screen+Shot+2017-02-01+at+12.03.18+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="230" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMdybxfNqt8t_bNLdRlOp-KxgoPNFseAAIpAq7YNJlv05226f_d2OZVldTR87Q2WQ30AyRCF-drZWYFMRzPVSxHKSTLTSCfL9r_MpG6Tn5N9a6JGcMNGfJY_-LdIeA7ltM85UiwMLsEvg/s400/Screen+Shot+2017-02-01+at+12.03.18+PM.png" width="400" /></a></div>
<div>
<br /></div>
Next, I search for a term that I know is only in some questions. Sure enough, I see the question name and the question content where matches occur. And clicking the link takes me to the appropriate questionnaire.<br />
<br />
Updating your plugin to use the new global search feature is a fairly easy process, especially if all you do is provide the basic search. There should be no reason not to do it!<br />
<br />
This concludes my series on adding global search to your Moodle plugin. Feel free to leave comments and suggestions. I will look at improving the question results as I noted above, and will provide updates when I have completed that.<br />
<br />
<div style="background-color: beige; border: 1px solid navy; color: navy; float: right; padding: 10px; width: 80%;">
<span style="font-family: inherit;"><b><i>Checklist for building additional plugin content searching:</i></b></span>
<br />
<ol>
<li style="text-align: left;"><span style="font-family: inherit;">Create a <a href="https://docs.moodle.org/dev/Search_API#Base_class" target="_blank">new class</a> with the name of your area in a similarly named file in your 'classes/search' directory.</span></li>
<li style="text-align: left;"><span style="font-family: inherit;">Add a language string <a href="https://docs.moodle.org/dev/Search_API#Name" target="_blank">name</a> using "search:<i>AREANAME</i>" for the search area in your plugin language file.</span></li>
<li style="text-align: left;"><span style="font-family: inherit;">Create a get_recordset_by_timestamp() function to retrieve the records you want to index.</span></li>
<li style="text-align: left;"><span style="font-family: inherit;">Create a get_document() function to return a completed document object to the search function.</span></li>
<li style="text-align: left;"><span style="font-family: inherit;">Create a check_access() function to determine a matched document's availability to the user performing the search.</span></li>
<li style="text-align: left;"><span style="font-family: inherit;">Create a get_context_url() function to return an appropriate Moodle URL to the matched document.</span></li>
<li style="text-align: left;"><span style="font-family: inherit;">Create a get_doc_url() function to return an appropriate Moodle URL to the matched document.</span></li>
</ol>
</div>
<div style="clear: both;">
<br />
<i>Update - 2017.02.02</i><br />
After a lot of discussion with other developers, I redid the activity search to include the question content. Since the question content was helping to find the questionnaire instance that contained it, it really made sense for it to be a part of the activity search and instance return. Since there could be many question records per questionnaire instance, I had to do the database search in the <span style="font-family: Courier New, Courier, monospace;">get_document()</span> function and add the question data there. You can see the resulting work <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/SEARCHFINAL/classes/search/activity.php" target="_blank">here</a>.</div>
<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-13821617395788098362017-01-26T10:24:00.001-05:002017-02-01T13:28:42.270-05:00Adding search to your Moodle plugin - part two<h3>
Part Two - Indexing Custom Information</h3>
In the last post, I created the basic code required to allow Moodle's global search feature search my plugin's "name" and "intro" information fields. Now I want to extend that functionality to include the "subtitle" and "additional info" fields.<br />
<br />
Reading the <a href="https://docs.moodle.org/dev/Search_API#Easy_case:_Activity_information" target="_blank">documentation</a>, this looks like it should be an extension of the functionality I wrote for the basic case. The information fields I want to return, both belong to an activity instance. And the case I already created is for that activity instance. So I want to look at extending the class I already created.<br />
<br />
If I decide to provide index data from other sources, then I would be creating new classes as described in the <a href="https://docs.moodle.org/dev/Search_API#Base_class" target="_blank">documentation following sections</a>.<br />
<br />
One of the concerns I have with my approach of continuing extending the <span style="font-family: "courier new" , "courier" , monospace;">base_activity</span> class, rather than the <span style="font-family: "courier new" , "courier" , monospace;">base_mod</span> class, is that the documentation says that I should use <span style="font-family: "courier new" , "courier" , monospace;">base_activity</span> "<i>to index Moodle activities basic data like the activity name and description</i>". And that <span style="font-family: "courier new" , "courier" , monospace;">base_mod</span> should be used for "<i>other specific activity data</i>". The two extra fields I want to include, aren't part of the main activity table of my plugin, but rather are stored in a second table called "questionnaire_survey". There is a one-to-one relationship though, and the information is considered part of the activity information, so I still think this the correct approach. But, to get that extra data, I will have to override the database function that gets all of that data.<br />
<br />
Looking at the <span style="font-family: "courier new" , "courier" , monospace;">/search/classes/base_activity.php</span> file, I see that the main function to retrieve and return the activity data is the <span style="font-family: "courier new" , "courier" , monospace;">get_recordset_by_timestamp</span> function:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_recordset_by_timestamp($modifiedfrom = 0) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> global $DB;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> return $DB->get_recordset_select($this->get_module_name(),</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> static::MODIFIED_FIELD_NAME . ' >= ?',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> array($modifiedfrom),</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> static::MODIFIED_FIELD_NAME . ' ASC');</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<br />
This is the one I will override in my class. In its current form, it returns all of the fields from the main activity table according to the "timemodified" field. Since I want to JOIN the "questionnaire_survey" with this table, I am going to have to be selective about the fields that are returned, as there is overlap.<br />
<br />
I open my <span style="font-family: "courier new" , "courier" , monospace;">questionnaire/classes/search/activity.php</span> file and add the following function (full file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/PART2/classes/search/activity.php#L45" target="_blank">here</a>):<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_recordset_by_timestamp($modifiedfrom = 0) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> global $DB;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;"> $sql = 'SELECT q.*, s.subtitle, s.info ' .</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'FROM {questionnaire} q ' .</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'WHERE q.timemodified >= ? ' .</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'ORDER BY q.timemodified ASC';</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;"> return $DB->get_recordset_sql($sql, [$modifiedfrom]);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<br />
Since I haven't told the search engine anywhere that I want to include the two new fields I've added, there must still be some changes to make. I install the new code, reindex the global search and test that things work. While there are no failures, I also cannot get any matches from the "subtitle" or "additional info" fields, confirming that there is more to do.<br />
<br />
Looking at the documentation again, the next function that is presented is <span style="font-family: "courier new" , "courier" , monospace;">get_document</span>. From the description, this function takes a record from the query results of <span style="font-family: "courier new" , "courier" , monospace;">get_recordset_by_timestamp</span> function, and constructs a <span style="font-family: "courier new" , "courier" , monospace;">\core_search\document</span> object with all of the data needed for indexing. My guess, is that I need to add my two new fields here. I'm going to override this function in my class.<br />
<br />
The main <a href="https://www.blogger.com/"><span id="goog_1518460908"></span>function<span id="goog_1518460909"></span></a> returns a constructed object with the data added to it as variables. I believe I can override this and take the returned function from the parent, add my new data to it, and return it. When I'm done, the function looks like this:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_document($record, $options = []) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> // Get the default implementation.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $doc = parent::get_document($record, $options);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;"> // Add the subtitle and additional info fields.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('subtitle', content_to_text($record->subtitle, false));</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('additionalinfo', content_to_text($record->info, $record->introformat));</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;"> return $doc;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<div>
<br /></div>
<div>
I add the code to my site, purge all the caches and reindex the search engine. But, searching for these fields is not returning my data. On the search areas admin page, I have been using "Update indexed contents". But, since none of my questionnaire activities have actually changed, nothing is being re-indexed. Instead, I need to use the "Reindex all site contents" button. When I do this, I get an error indicating:</div>
<div>
<br /></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> Coding error detected, it must be fixed by a programmer: "subtitle" field does not exist.</span></div>
<div>
<br /></div>
<div>
It would appear that using "<span style="font-family: "courier new" , "courier" , monospace;">subtitle</span>" is not correct.</div>
<div>
<br /></div>
<div>
The documentation shows an example of using <span style="font-family: "courier new" , "courier" , monospace;">get_document()</span>, and it uses "<span style="font-family: "courier new" , "courier" , monospace;">description1</span>" and "<span style="font-family: "courier new" , "courier" , monospace;">description2</span>" as the fields being set. Its likely that I can't use "<span style="font-family: "courier new" , "courier" , monospace;">subtitle</span>" and "<span style="font-family: "courier new" , "courier" , monospace;">additionalinfo</span>" as field names in the <span style="font-family: "courier new" , "courier" , monospace;">$doc->set()</span> function.</div>
<div>
<br /></div>
<div>
The <span style="font-family: "courier new" , "courier" , monospace;">\core_search\document</span> class is defined in the <span style="font-family: "courier new" , "courier" , monospace;">/search/classes/document.php</span> file. The <span style="font-family: "courier new" , "courier" , monospace;">set()</span> function uses a static array to look for specific field names it is allowed to set. The names I used, "<span style="font-family: "courier new" , "courier" , monospace;">subtitle</span>" and "<span style="font-family: "courier new" , "courier" , monospace;">additionalinfo</span>" are not part of those. However "<span style="font-family: "courier new" , "courier" , monospace;">description1</span>" and "<span style="font-family: "courier new" , "courier" , monospace;">description2</span>" are. This confirms that I need to change those functions. The <span style="font-family: "courier new" , "courier" , monospace;">set()</span> function throws an exception when an unknown field is used, and that explains the error I saw.</div>
<div>
<br /></div>
<div>
My new function now looks like this (full file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/PART2/classes/search/activity.php#L67" target="_blank">here</a>):</div>
<div>
<br /></div>
<div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> public function get_document($record, $options = []) {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Get the default implementation.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc = parent::get_document($record, $options);</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> // Add the subtitle and additional info fields.</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('description1', content_to_text($record->subtitle, false));</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $doc->set('description2', content_to_text($record->info, $record->introformat));</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> return $doc;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
</div>
<div>
<br /></div>
<div>
When I load it, and reindex all of the site contents, my search works. I now see content found from a subtitle.</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJO7CGc6V3Fs7LEc2VCj9YrrKgxoHQCVUadkBqx4ToNOhkHyX2U9S2zyozdXnwaEMJD1xYWgS-3xnwjWzvTZSd30PVejCLHitxTU4zrttu4AYkh0_zo3aOA1yzxX5BdL2wLwUzlVgDPaw/s1600/Screen+Shot+2017-01-24+at+2.52.49+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="244" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJO7CGc6V3Fs7LEc2VCj9YrrKgxoHQCVUadkBqx4ToNOhkHyX2U9S2zyozdXnwaEMJD1xYWgS-3xnwjWzvTZSd30PVejCLHitxTU4zrttu4AYkh0_zo3aOA1yzxX5BdL2wLwUzlVgDPaw/s320/Screen+Shot+2017-01-24+at+2.52.49+PM.png" width="320" /></a></div>
<div>
<br /></div>
<div>
So it seems that I can add indexed fields to the a activity level search, but only two. And they need to be referred to as "<span style="font-family: "courier new" , "courier" , monospace;">description1</span>" and "<span style="font-family: "courier new" , "courier" , monospace;">description2</span>". I'm guessing to add more than that, I would need to create more classes using <span style="font-family: "courier new" , "courier" , monospace;">base_mod</span> instead of <span style="font-family: "courier new" , "courier" , monospace;">base_activity</span>.<br />
<br />
I'll explore that more in the <a href="http://tandl.churchward.ca/2017/02/adding-search-to-your-plugin-part-three.html" target="_blank">next post</a>, as well as how to search for more parts of my plugin.</div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-54062688033297987512017-01-23T15:29:00.004-05:002017-01-26T10:26:44.907-05:00Adding search to your Moodle plugin<h3>
Part One - The Basics</h3>
In Moodle 3.1, the "<a href="https://docs.moodle.org/31/en/Global_search" target="_blank">Global Search</a>" feature was introduced. This feature provides the ability for a user to search for text within the entire site for entered terms. The results obey all role and capability rules, so only parts that can be legitimately accessed by the requesting user are returned.<br />
<br />
As delivered, core activities are included in the global search. But plugins need to provide code to the <a href="https://docs.moodle.org/dev/Search_API" target="_blank">search API</a> in order to be included in the search results. In this series of blog posts, I am going to add the search feature to my questionnaire plugin.<br />
<br />
Some resources that will help with this task are:<br />
<br />
<ul>
<li><a href="https://docs.moodle.org/31/en/Global_search" target="_blank">Global search</a></li>
<li><a href="https://docs.moodle.org/dev/Search_API" target="_blank">Search API</a></li>
<li><a href="https://docs.moodle.org/dev/Search_engines" target="_blank">Search engines</a></li>
</ul>
<br />
<div>
The global search feature uses search engine plugins to provide the search functions. Currently, there is only one plugin, that uses an external technology called Solr. In order to use search, and enable me to test my work, I will install it in my development environment. Moodle provides <a href="https://docs.moodle.org/31/en/Global_search#Setting_up_Solr" target="_blank">documentation</a> to do this. Since I am using OSX with Macports, I follow those instructions. After that, I use the administration functions in my Moodle site to enable global search and verify that it is working. I'm not going to go into the details of that here, but the documentation links were enough for me to successfully do this.</div>
<div>
<br /></div>
<div>
Before I start, I'm going to use the search function to see how it works with the existing activities, and verify that it is not working for mine. I'm using Moodle 3.2 with the Boost theme. The search function is located in the upper right part of the navigation bar, with a text box entry next to a magnifying glass icon as below:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxFVCIhBUfXiu-MJn23mnD789GzREcLCMO9owvTGUe88D5H68OSJACis1NBIR5iROKURf_pYekAFpaP8519cut8L2WJ0IB0tTlg9Zz2B_EQiUgA7Qi0DHqKt_RbwsQeOoud7qsweSKAR0/s1600/Screen+Shot+2017-01-23+at+1.38.26+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="" border="0" height="56" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxFVCIhBUfXiu-MJn23mnD789GzREcLCMO9owvTGUe88D5H68OSJACis1NBIR5iROKURf_pYekAFpaP8519cut8L2WJ0IB0tTlg9Zz2B_EQiUgA7Qi0DHqKt_RbwsQeOoud7qsweSKAR0/s400/Screen+Shot+2017-01-23+at+1.38.26+PM.png" title="Navigation search box" width="400" /></a></div>
<div>
<br /></div>
<div>
Once I enter my search text, for example "test", the screen displays everything it found and some further options. Under the "filter" section, there is a "Search area" drop down that lists all of the areas available to search within (see image below). My plugin does not appear there, so I presume this will be one of the places that I will see a change once I have enabled search in my plugin.</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidM_sGAfN-01BciAkzRkNNWVC70Di82kOg5ceEsitVcJPmizxj_Wr5ca2BJOJyosPiFWlrlPfIucUGCRxefTPT-DxI4zz8MrD7e2UgRDAGx2Kh6odlP1Vcbm829MlwRVUMk4zcvlxWcjc/s1600/Screen+Shot+2017-01-23+at+1.42.37+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img alt="" border="0" height="201" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidM_sGAfN-01BciAkzRkNNWVC70Di82kOg5ceEsitVcJPmizxj_Wr5ca2BJOJyosPiFWlrlPfIucUGCRxefTPT-DxI4zz8MrD7e2UgRDAGx2Kh6odlP1Vcbm829MlwRVUMk4zcvlxWcjc/s320/Screen+Shot+2017-01-23+at+1.42.37+PM.png" title="Global search form and search area filter options." width="320" /></a></div>
<div>
<br /></div>
<div>
I also notice that in those filter choices, some of the activities show only "activity information" while others show this and other levels of the activity. These correspond to what content an activity returns to the search function. Looking at the <a href="https://docs.moodle.org/dev/Search_API#Easy_case:_Activity_information" target="_blank">documentation</a>, I see that returning the "activity information" is the easiest case, and does not require much work at all. I am going to start there.</div>
<div>
<br /></div>
<div>
I have created a working branch off of my latest Moodle 3.2 branch called GLOBALSEARCH_32 as my starting point. You can follow along on Github <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/GLOBALSEARCH_32" target="_blank">here</a>.</div>
<div>
<br /></div>
<div>
The documentation makes this look pretty straight forward. I create a subdirectory called <span style="font-family: "courier new" , "courier" , monospace;">search</span> in my plugin's <span style="font-family: "courier new" , "courier" , monospace;">classes</span> subdirectory. In that new directory, I add a file named <span style="font-family: "courier new" , "courier" , monospace;">activity.php</span>. The contents, are basically this (you can see the entire file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/PART1/classes/search/activity.php" target="_blank">here</a>):</div>
<div>
<br /></div>
<div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> namespace mod_questionnaire\search;</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> class activity extends \core_search\base_activity {</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> }</span></div>
</div>
<div>
<br /></div>
<div>
I'm going to update my development site with this new code, and see what happens. After copying, I check the filter dropdown as above, but I don't see the questionnaire in the list. In all likelihood, I need to purge all of the caches. After doing that, the page reloads, but gives me the following error:</div>
<div>
<br /></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;">Invalid get_string() identifier: 'search:activity' or component 'mod_questionnaire'. Perhaps you are missing $string['search:activity'] = ''; in mod/questionnaire/lang/en/questionnaire.php?</span></div>
<div>
<ul>
<li><span style="font-family: "courier new" , "courier" , monospace;">line 349 of /lib/classes/string_manager_standard.php: call to debugging()</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">line 7076 of /lib/moodlelib.php: call to core_string_manager_standard->get_string()</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">line 146 of /search/classes/base.php: call to get_string()</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">line 62 of /search/classes/output/form/search.php: call to core_search\base->get_visible_name()</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">line 204 of /lib/formslib.php: call to core_search\output\form\search->definition()</span></li>
<li><span style="font-family: "courier new" , "courier" , monospace;">line 59 of /search/index.php: call to moodleform->__construct()</span></li>
</ul>
</div>
<div>
It looks like I need to define a specific language string as well. The error nicely tells me exactly what I need. I add the line:</div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> $string['search:activity'] = 'Questionnaire - activity information';</span></div>
<div>
to the <span style="font-family: "courier new" , "courier" , monospace;">lang/en/questionnaire.php</span> file and try again. This time, I have no error, and the drop down shows my activity in the list:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVH0srcIJUxBqXQKxEgU6WSeNRXDztRmtnKl3VsqndTnE5XHA2qrUPE9IiYrlt6Z4HZxp7f39KzLjBODGM0dO0Xl5O9mIhAT0P5yDekx_CWV5BdPVuBCtKNGhhCiee3nWh96KKVZPS7O4/s1600/Screen+Shot+2017-01-23+at+3.04.00+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVH0srcIJUxBqXQKxEgU6WSeNRXDztRmtnKl3VsqndTnE5XHA2qrUPE9IiYrlt6Z4HZxp7f39KzLjBODGM0dO0Xl5O9mIhAT0P5yDekx_CWV5BdPVuBCtKNGhhCiee3nWh96KKVZPS7O4/s320/Screen+Shot+2017-01-23+at+3.04.00+PM.png" width="320" /></a></div>
<div>
<br /></div>
<div>
However, when I perform a search that should include a questionnaire, it does not show up. The most likely reason is that the search engine has not indexed the plugin into its database yet. The global search can be scheduled to reindex its database, and it can also be manually reindexed. At the main global search administration page, <span style="font-family: "courier new" , "courier" , monospace;">admin/settings.php?section=manageglobalsearch</span>, there is an "<span style="font-family: "courier new" , "courier" , monospace;">Index data</span>" link. Reviewing that page, I can see that there is a section for "<span style="font-family: "courier new" , "courier" , monospace;">Questionnaire - activity information</span>", that is showing as never having been indexed. When I click the "<span style="font-family: "courier new" , "courier" , monospace;">Update indexed contents</span>" button, also on that page, my questionnaire now shows as having been indexed. I perform my search again, and I can now see results from my questionnaire.</div>
<div>
<br /></div>
<div>
Adding the basic search functionality turned out to be extremely easy. I simple had to add one file that extended a class, and one language string. In the <a href="http://tandl.churchward.ca/2017/01/adding-search-to-your-moodle-plugin_26.html" target="_blank">next post</a>, I'll add extra levels of searching to include questionnaire "subtitle" and "additional info" sections.</div>
<div>
<br /></div>
<div>
<br /></div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-38408376796520092712017-01-17T11:35:00.000-05:002017-01-17T11:35:39.371-05:00Part 5 - Further discussionAfter I completed my first attempt at adding renderers and templates to my questionnaire plugin, I went on to use them much more thoroughly in the actual release. The <a href="https://github.com/remotelearner/moodle-mod_questionnaire/tree/3.2.0" target="_blank">Moodle 3.2 release of questionnaire</a> uses templates for all of the <a href="https://github.com/remotelearner/moodle-mod_questionnaire/tree/3.2.0/templates" target="_blank">major pages</a>. This is a work in progress, and I have taken some shortcuts to make the transition easier while allowing me to release the code as it is worked on.<br />
<br />
Most noticeably, since the output of questionnaire is generated by specific functions that return HTML pieces, I have created the templates to accept that HTML. This means that only the top level formatting of a page is templatable. But, I am working on extracting more and more of the output into the templates as the work continues.<br />
<br />
In the <a href="https://github.com/remotelearner/moodle-mod_questionnaire/tree/RENDERER_32" target="_blank">RENDERER_32 work in progress branch</a>, I have refactored and extracted almost all of the question specific output into individual <a href="https://github.com/remotelearner/moodle-mod_questionnaire/tree/RENDERER_32/templates" target="_blank">question type templates</a>. These templates are used with renderer functions to return the HTML to the main renderer. I have <a href="https://github.com/remotelearner/moodle-mod_questionnaire/blob/RENDERER_32/classes/output/renderer.php#L171" target="_blank">coded the renderer function</a> so that it will use question templates as I define them, or fallback to the old method if they are not present. If you have questions about this work, contact me or post a question here.<br />
<br />
If you wander through those templates, you'll see some other aspects of templates used that I didn't discuss in this series. For example, in the <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/remotelearner/moodle-mod_questionnaire/blob/RENDERER_32/templates/navbaralpha.mustache" target="_blank">navbaralpha.mustache</a></span> file you can see the use of the language string <span style="font-family: Courier New, Courier, monospace;">{{# str}}</span> <a href="https://github.com/remotelearner/moodle-mod_questionnaire/blob/RENDERER_32/templates/navbaralpha.mustache#L61" target="_blank">helper function</a>. This is a way to put a language string directly into the template. You can find more information on that <a href="https://docs.moodle.org/dev/Templates#.7B.7B.23_str_.7D.7D" target="_blank">here</a>.<br />
<br />
In the <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/remotelearner/moodle-mod_questionnaire/blob/RENDERER_32/templates/question_check.mustache" target="_blank">question_check.mustache</a></span> template, you can see the use of dotted notation for tag names. For example, <span style="font-family: Courier New, Courier, monospace;"><a href="https://github.com/remotelearner/moodle-mod_questionnaire/blob/RENDERER_32/templates/question_check.mustache#L38" target="_blank">choice.id</a></span>. While not strictly necessary, I find that this notation helps to document the structure of the context data being used in the template. In this example, the fact that <span style="font-family: Courier New, Courier, monospace;">id</span> is a subelement of the <span style="font-family: Courier New, Courier, monospace;">choice</span> structure.<br />
<br />
Regarding data structures, if you recall in "Part three", I mentioned that the <a href="http://tandl.churchward.ca/2017/01/part-3-setting-up-templates_59.html#dataformats" target="_blank">template data could be formatted as arrays or objects</a> and that both would work. The <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/BUILD_RENDERERS/classes/output/indexpage.php#L68" target="_blank">code I provided</a> used strictly arrays. But it could have used objects as well as the one array needed for the <span style="font-family: Courier New, Courier, monospace;">rows</span> element. Below is an alternate coding using objects instead of arrays where possible:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> /**</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * Prepare data for use in a template</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> *</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param \renderer_base $output</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @return stdClass</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> */</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function export_for_template(\renderer_base $output) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data = new \stdClass();</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data->headings = new \stdClass();</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> foreach ($this->headings as $key => $heading) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data->headings->{$key} = $heading;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data->rows = [];</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> foreach ($this->rows as $row) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> list($topic, $name, $responses, $type) = $row;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $item = new \stdClass();</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $item->topic = $topic;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $item->name = $name;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $item->responses = $responses;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $item->type = $type;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data->rows[] = $item;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> return $data;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<div>
<br /></div>
<div>
Lastly, I never got into using javascript with templates. I have personally not spent enough time with this to do that topic justice. If you wish to explore that though, start with the <a href="https://docs.moodle.org/dev/Templates" target="_blank">documentation</a>. Additionally Damyon Wiese from Moodle HQ <a href="https://www.youtube.com/watch?v=UTePjRZqAg8" target="_blank">gives a great demonstration</a> that covers how to include javascript and Ajax with templates, as well as a great introduction to templates in general. If you want to skip ahead to where javascript comes into play, fast-forward to around 14:10 in the video.<br />
<br />
If you want to discuss any of this series, or recommend alternate approaches, please feel free to leave comments or contact me directly.</div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-32787607223093159182017-01-13T13:49:00.000-05:002017-06-15T11:06:18.555-04:00Part 4 - Using the TemplateIn the previous posts, I created and used a renderer, and created a simple mustache template for my plugin. In part four of the "Adding renderers and templates to your Moodle plugin" series, I will present how to code the template classes and then use them in the renderer.<br />
<br />
To use a template, I need a few different constructs.<br />
<br />
Firstly, I need a new class that implements the core <span style="font-family: "courier new" , "courier" , monospace;">renderable</span> and <span style="font-family: "courier new" , "courier" , monospace;">templatable</span> classes. The file and class name should be the same as the template file name to make things easier. And the file must be located in the <span style="font-family: "courier new" , "courier" , monospace;">classes/output/</span> directory.<br />
<br />
Since my template file is called <span style="font-family: "courier new" , "courier" , monospace;">indexpage.mustache</span>, I create a new file called <span style="font-family: "courier new" , "courier" , monospace;">indexpage.php</span> in the <span style="font-family: "courier new" , "courier" , monospace;">classes/output</span> directory, and add a class called <span style="font-family: "courier new" , "courier" , monospace;">indexpage</span> that implements <span style="font-family: "courier new" , "courier" , monospace;">\renderable</span> and <span style="font-family: "courier new" , "courier" , monospace;">\templatable</span>. The class declaration must look like this (the entire file is available <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/classes/output/indexpage.php" target="_blank">here</a>):<br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;"> class indexpage implements \renderable, \templatable {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<br />
The purpose of this script is to collect the output data for the template and make it available to the renderer. The only real required function is the <span style="font-family: "courier new" , "courier" , monospace;">export_for_template</span> function, as renderers require that function to retrieve the data structure that is passed into the template. Other functions can be provided as necessary including those that process the data for the template. The code for the export function looks like this:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function export_for_template(\renderer_base $output) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data = ['headings' => [], 'rows' => []];</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> foreach ($this->headings as $key => $heading) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data['headings'][$key] = $heading;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> foreach ($this->rows as $row) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> list($topic, $name, $responses, $type) = $row;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data['rows'][] =['topic' => $topic, 'name' => $name,</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'responses' => $responses, 'type' => $type];</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> return $data;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<br />
For my version, I pass all the necessary data for the template to the <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/classes/output/indexpage.php#L51" target="_blank">constructor</a>, and then process it for output in the export function. The class contains two property variables, <span style="font-family: "courier new" , "courier" , monospace;">$headings</span> and <span style="font-family: "courier new" , "courier" , monospace;">$rows</span>, which will hold the table headings and each row of content for the index page based on what is passed into the constructor. My <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/classes/output/indexpage.php#L69" target="_blank">export_for_template</a></span> function processes these variables and creates the expected data construct for the template.<br />
<br />
If you recall from the last post, my <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/templates/indexpage.mustache" target="_blank">template</a> expects a data structure with two top level elements: <span style="font-family: "courier new" , "courier" , monospace;">#headings</span> and <span style="font-family: "courier new" , "courier" , monospace;">#rows</span>. In the <span style="font-family: "courier new" , "courier" , monospace;">export_for_template</span> function, I create a PHP array called <span style="font-family: "courier new" , "courier" , monospace;">$data</span>, and add the keys <span style="font-family: "courier new" , "courier" , monospace;">'headings'</span> and <span style="font-family: "courier new" , "courier" , monospace;">'rows'</span> to it, each of which is initialized to an array. Then, I load up the <span style="font-family: "courier new" , "courier" , monospace;">headings</span> array with a value for each '<span style="font-family: "courier new" , "courier" , monospace;">title[n]</span>' index and the <span style="font-family: "courier new" , "courier" , monospace;">rows</span> array with an array for each content row containing the <span style="font-family: "courier new" , "courier" , monospace;">topic</span>, <span style="font-family: "courier new" , "courier" , monospace;">name</span>, <span style="font-family: "courier new" , "courier" , monospace;">responses</span> and <span style="font-family: "courier new" , "courier" , monospace;">type</span> elements. So after processing, the function returns a structure that looks something like this (for example):<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data['headings' =></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> ['title1' => 'Topic',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'title2' => 'Name',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'title3' => 'Responses',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'title4' => 'Questionnaire Type'</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> ],</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'rows' =></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> [0 =></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> ['topic' => 'Topic1',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'name' => 'Questionnaire1',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'responses' => 42,</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'type' => 'Private'</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> ],</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> [1 =></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> ['topic' => 'Topic2',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'name' => 'Questionnaire2',</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'responses' => 11,</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> 'type' => 'Private'</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> ]</span><br />
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> ]</span></div>
<div>
<span style="font-family: "courier new" , "courier" , monospace;"> ];</span></div>
<br />
This structure is what gets sent to the template to meet its requirements for data.<br />
<br />
Now that I have the necessary implementation of the core \<span style="font-family: "courier new" , "courier" , monospace;">templatable</span> class, and a template, I need to code the renderer to use them. Currently, the renderer's <span style="font-family: "courier new" , "courier" , monospace;"><a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/JUSTRENDERERANDNS/classes/output/renderer.php#L38" target="_blank">render_index</a></span> function, creates the HTML output from Moodle core helper functions, specifically the <span style="font-family: "courier new" , "courier" , monospace;">\html_table</span> class. I need to change that so it uses the template and template class.<br />
<br />
To do that, I change the declaration of the function to take one argument, a <span style="font-family: "courier new" , "courier" , monospace;">\templatable</span> object. And I get rid of the current code, and replace it with the object's <span style="font-family: "courier new" , "courier" , monospace;">export_for_template</span> function and a call to the renderer class function <span style="font-family: "courier new" , "courier" , monospace;">render_from_template</span>. The new function looks like this (you can see the entire file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/classes/output/renderer.php" target="_blank">here</a>):<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function render_indexpage(\templatable $indexpage) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data = $indexpage->export_for_template($this);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> return $this->render_from_template('mod_questionnaire/indexpage', $data);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<br />
The new <span style="font-family: "courier new" , "courier" , monospace;">render_index</span> function expects an object created from the <span style="font-family: "courier new" , "courier" , monospace;">classes/output/indexpage.php::indexpage</span> class. With that object, it will call the <span style="font-family: "courier new" , "courier" , monospace;">export_for_template</span> function to get the data necessary to pass into the template. Then it will pass that data to the template to get the HTML output from the core <span style="font-family: "courier new" , "courier" , monospace;">render_from_template</span> function. The <span style="font-family: "courier new" , "courier" , monospace;">render_from_template</span> function takes two arguments: the template to use and the data to pass the template. In my case, the template to use is defined by the argument '<span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire/indexpage</span>'. The '<span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire</span>' portion tells the function that this is for an activity module plugin, and therefore the template should be found in the directory '<span style="font-family: "courier new" , "courier" , monospace;">/mod/questionnaire/templates/</span>'. The '<span style="font-family: "courier new" , "courier" , monospace;">indexpage</span>' portion indicates that there should be file named '<span style="font-family: "courier new" , "courier" , monospace;">indexpage.mustache</span>' in that directory. After that, the function executes the necessary magic to pass the data into the template and return the final formatted HTML.<br />
<br />
The final step I need to do in order to use all of this new template code is change the way I am using the renderer in the <span style="font-family: "courier new" , "courier" , monospace;">index.php</span> file. In this case, I only need to change the code where I am actually using the renderer to output to the screen. I need to do two things: change the arguments being passed to the <span style="font-family: "courier new" , "courier" , monospace;">render_index</span> function and create an <span style="font-family: "courier new" , "courier" , monospace;">indexpage</span> templatable object for that function to use.<br />
<br />
<a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/JUSTRENDERERANDNS/index.php#L214" target="_blank">The old code looks like this</a>:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> echo $output->render_index($headings, $align, $content);</span><br />
<br />
<a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/index.php#L215" target="_blank">The new code looks like this</a>:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> $indexpage = new \mod_questionnaire\output\indexpage($headings, $content);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> echo $output->render_indexpage($indexpage);</span><br />
<br />
Note, although I changed the function name from <span style="font-family: "courier new" , "courier" , monospace;">render_index</span> to <span style="font-family: "courier new" , "courier" , monospace;">render_indexpage</span>, it was not necessary. I changed the name only because I felt it better reflected the functionality.<br />
<br />
I now have everything I need to output my plugin's index page using renderers and templates. And, other developers can now easily change the way that page display looks using alternate templates or renderers in their themes without having to change any of the plugin's code. The entire plugin's code using this template can be viewed <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/FOURTHTEMPLATE" target="_blank">here</a>.<br />
<br />
In my <a href="http://tandl.churchward.ca/2017/01/part-5-further-discussion.html">next post</a> I will look at alternate coding strategies and how I used renderers and templates for the rest of the plugin's output code.<br />
<br /><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com15tag:blogger.com,1999:blog-7094986855457289958.post-8769667854164111332017-01-11T14:50:00.000-05:002017-01-17T11:18:25.964-05:00Part 3 - Setting up TemplatesIn part three of the "Adding renderers and templates to your Moodle plugin" series, I will present how to set up and begin using templates with your plugin renderer.<br />
<br />
At this point, in parts one and two, I have created a renderer for my plugin's index page, and modified the output code for the index page to use the renderer.<br />
<br />
There are a number of parts to implementing templates. I need a renderer. I did this in the earlier parts of the series. I need output classes based on Moodle's <span style="font-family: "courier new" , "courier" , monospace;">\renderable</span> and <span style="font-family: "courier new" , "courier" , monospace;">\templatable</span> classes. And I need Mustache templates to define the actual HTML output.<br />
<br />
The first thing I am going to do is construct the mustache <a href="https://docs.moodle.org/dev/Templates">template</a>. A mustache template is a file that looks very much like an HTML file. This makes it easy for designer/developers, familiar with HTML to create a layout display. They do not need knowledge of PHP or Moodle internals.<br />
<br />
In my plugin, I need to create a new subdirectory called "templates" to hold the mustache templates I create. Any template file I create requires the ".mustache" extension. Moodle's output system is set up to use the component name and the <span style="font-family: "courier new" , "courier" , monospace;">templates</span> subdirectory name to locate any templates I specify in the specific functions. Since I am creating one for the index page, I create a file called <span style="font-family: "courier new" , "courier" , monospace;">templates/indexpage.mustache</span>.<br />
<br />
If you recall from the renderer posts, my index page was essentially an HTML table, listing the questionnaire instances in the course it was being accessed for. I used the <span style="font-family: "courier new" , "courier" , monospace;">html_writer::table function</span> to generate the HTML of this table with the data I loaded. My new template needs to do something similar, except I will have to write the HTML for the table listing using HTML code in the mustache file.<br />
<br />
The file I create contains the following HTML/Mustache code (you can see the whole file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/templates/indexpage.mustache">here</a>):<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"><table class="generaltable"></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <thead></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <tr></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>{{#headings}}</b></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <th class="header" style="text-align:left;" scope="col"><b>{{title1}}</b></th></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <th class="header" style="text-align:left;" scope="col"><b>{{title2}}</b></th></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <th class="header" style="text-align:center;" scope="col"><b>{{title3}}</b></th></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <th class="header" style="text-align:left;" scope="col"><b>{{title4}}</b></th></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>{{/headings}}</b></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> </tr></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> </thead></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <tbody></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>{{#rows}}</b></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <tr></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <td class="cell" style="text-align:left;" scope="col"><b>{{topic}}</b></td></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <td class="cell" style="text-align:left;" scope="col"><b>{{{name}}}</b></td></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <td class="cell" style="text-align:center;" scope="col"><b>{{responses}}</b></td></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> <td class="cell" style="text-align:left;" scope="col"><b>{{type}}</b></td></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> </tr></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><b>{{/rows}}</b></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> </tbody></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"></table></span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
You can see from this that the mustache template file looks very much like part of an HTML file, except for the portions contained within multiple brace brackets ("<span style="font-family: "courier new" , "courier" , monospace;">{{ }}</span>"). Anything contained within the brace bracket structures, are mustache constructs that will be replaced by real data when the template is executed.<br />
<br />
When a template is executed, it is passed a data structure. The data structure is where the template gets the real data to substitute in place of its brace tagged variables. Deconstructing this template, I have two main data structures: <span style="font-family: "courier new" , "courier" , monospace;">headings</span> and <span style="font-family: "courier new" , "courier" , monospace;">rows</span>. These structures are contained within opening and closing tag pairs, specifically:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> {{#headings}} {{/headings}}</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> {{#rows}} {{/rows}}</span><br />
<br />
Each of these structures contains other data defined by tags. The <span style="font-family: "courier new" , "courier" , monospace;">headings</span> structure contains<br />
<span style="font-family: "courier new" , "courier" , monospace;">{{title1}}</span>, <span style="font-family: "courier new" , "courier" , monospace;">{{title2}}</span>, <span style="font-family: "courier new" , "courier" , monospace;">{{title3}}</span> and <span style="font-family: "courier new" , "courier" , monospace;">{{title4}}</span>. The <span style="font-family: "courier new" , "courier" , monospace;">rows</span> structure contains <span style="font-family: "courier new" , "courier" , monospace;">{{topic}}</span>, <span style="font-family: "courier new" , "courier" , monospace;">{{name}}</span>, <span style="font-family: "courier new" , "courier" , monospace;">{{responses}}</span> and <span style="font-family: "courier new" , "courier" , monospace;">{{type}}</span>. This level of the structure is the actual data. When the template is executed, it will expect a data structure that contains these structures passed into it.<br />
<br />
Putting the data into section tags allows for some logic to occur within the template. If the section tag exists as a variable in the passed in data, then the output contained within the tag will be displayed. If the section tag does not exist, then the output contained within the tag will not be displayed. Additionally, if the section tag variable is an array, then the output within it will be repeated for each item of the section tag array. For my template, this is important for the <span style="font-family: "courier new" , "courier" , monospace;">rows</span> variable, since there may be multiple rows of data.<br />
<br />
In the case of the <span style="font-family: "courier new" , "courier" , monospace;">headings</span> section, if the data contains no <span style="font-family: "courier new" , "courier" , monospace;">headings</span> variable, a table row with no columns will be displayed in the <span style="font-family: "courier new" , "courier" , monospace;"><thead></span> section. In the case of the <span style="font-family: "courier new" , "courier" , monospace;">rows</span> section, one row containing four columns each (one each of <span style="font-family: "courier new" , "courier" , monospace;">topic</span>, <span style="font-family: "courier new" , "courier" , monospace;">name</span>, <span style="font-family: "courier new" , "courier" , monospace;">responses</span> and <span style="font-family: "courier new" , "courier" , monospace;">type</span>) will be displayed for each element of the <span style="font-family: "courier new" , "courier" , monospace;">rows</span> array. If <span style="font-family: "courier new" , "courier" , monospace;">rows</span> is not present in the data, then no content rows will be displayed.<br />
<br />
For this template, since I know I am always going to be passing in the table headings, I really don't need the <span style="font-family: "courier new" , "courier" , monospace;">#header</span> section and structure at all. I could simply pass in the <span style="font-family: "courier new" , "courier" , monospace;">title</span> variables as standalone variables in the template data structure. In that case, I would rewrite the table header section without the opening and closing <span style="font-family: "courier new" , "courier" , monospace;">header</span> tags.<br />
<br />
While I haven't discussed the code that constructs the data to be sent to the template yet, I just want to point out some details about how that data can look. For this, assume <span style="font-family: "courier new" , "courier" , monospace;">$data</span> is the data variable I am constructing to pass to the template.<br />
<br />
<a href="https://www.blogger.com/null" name="dataformats"></a>The template will expect the data that is passed in to be either an object with each template tag defined as a property (object variable), or as an array with each template tag defined as an array index. In the case of this template, the data structure expected will consist of either:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data->headings</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data->rows</span><br />
or:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data['headings']</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $data['rows']</span><br />
or a combination of both.<br />
<br />
The <span style="font-family: "courier new" , "courier" , monospace;">headings</span> variable, likewise can be either an object or an array. So for example, either<br />
<span style="font-family: "courier new" , "courier" , monospace;">headings['title1']</span> or <span style="font-family: "courier new" , "courier" , monospace;">headings->title1</span> will work successfully with the template data.<br />
<br />
For this template, if there are multiple rows of data to be displayed, then the <span style="font-family: "courier new" , "courier" , monospace;">rows</span> variable must be an array structure. Each array item can be either another array or an object however. So, for this template for example, we expect either <span style="font-family: "courier new" , "courier" , monospace;">rows[0]['topic']</span> or <span style="font-family: "courier new" , "courier" , monospace;">rows[0]->topic</span>. If <span style="font-family: "courier new" , "courier" , monospace;">rows</span> is not an array, there will only ever be one row displayed.<br />
<br />
The data structure required is documented as a JSON structure in the <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/FOURTHTEMPLATE/templates/indexpage.mustache">template file</a> as comments before the code.<br />
<br />
One other thing to point out before I move on. You may have noticed that all of the template variable tags are enclosed in two brace brackets, except for one. The <span style="font-family: "courier new" , "courier" , monospace;">name</span> variable is enclosed in three brace brackets - <span style="font-family: "courier new" , "courier" , monospace;">{{{name}}}</span>. When a variable enclosed in two brace brackets is substituted for the real data, it is HTML escaped. That means only text will be displayed, and any HTML constructs contained in that variable will be converted to escaped text and displayed verbatim rather than interpreted. When a variable is enclosed in three brace brackets is substituted for the real data, the data is included raw. That means the HTML will be interpreted and displayed accordingly. This is useful when you want to display a block of HTML formatted content.<br />
<br />
In the case of my template, the <span style="font-family: "courier new" , "courier" , monospace;">name</span> variable also contains a link to the actual questionnaire instance, so I want the HTML to be displayed as I passed it. In actuality, I should probably rewrite the template so that the pieces of the link are passed instead of the entire HTML structure. Generally speaking, it is safer and better practice to pass only unformatted text.<br />
<br />
In the <a href="http://tandl.churchward.ca/2017/01/part-4-using-template.html" target="_blank">next post</a>, I will begin modifying the renderer code to use the template.<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com7tag:blogger.com,1999:blog-7094986855457289958.post-35520980607794726752017-01-09T09:07:00.000-05:002017-01-11T14:50:33.692-05:00Part 2 - Using the Renderer<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">In part two of the "Adding renderers and templates to your Moodle plugin" series, I will present how to use the renderer I created in part one in my plugin.</span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"><br /></span>
<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">To summarize what I completed in part one, I created a renderer for my questionnaire module plugin using the </span><span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">classes/output/</span><span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"> structure and namespaces. This construction allows my classes to take advantage of Moodle's autoloading.</span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"><br /></span>
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><a href="https://plus.google.com/u/0/+DamyonWiese" style="font-family: arial, tahoma, helvetica, freesans, sans-serif;">Damyon Wiese</a><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">, one of the Moodle HQ leads, <a href="https://plus.google.com/+DamyonWiese/posts/exMTf9BrAmU">pointed out an error I made in part one</a>. I showed a version of the renderer that used the class name "</span><span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire_renderer</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">" without namespaces, in the file "</span><span style="font-family: "courier new" , "courier" , monospace;">renderer.php</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">" located in the </span><span style="font-family: "courier new" , "courier" , monospace;">classes/output/</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> directory. Apparently, this will not work in a final solution. If I don't use namespaces, then that file must be located in either the plugin root or in </span><span style="font-family: "courier new" , "courier" , monospace;">classes/</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">. In any case, it is always best to use the namespace solution that I finished with. I have updated <a href="http://tandl.churchward.ca/2017/01/adding-renderers-and-templates-to-your_5.html">Part 1</a> to reflect the correct structures.</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"><br /></span></span></span><span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">Now I am going to edit the </span><span style="font-family: "courier new" , "courier" , monospace;">index.php</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> file to use the renderer I created.</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">In the pre-renderer index file, I used PHP's </span><span style="font-family: "courier new" , "courier" , monospace;">echo</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> statement with Moodle's global </span><span style="font-family: "courier new" , "courier" , monospace;">$OUTPUT</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> object and </span><span style="font-family: "courier new" , "courier" , monospace;">html_writer</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> helper class to display the screen output. The global </span><span style="font-family: "courier new" , "courier" , monospace;">$OUTPUT</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> object is the global renderer object available to any script in Moodle. If a script is not using renderers, it will likely use this object to perform its necessary output operations. When a plugin uses its own renderer, it instantiates its own renderer object and uses it in place of </span><span style="font-family: "courier new" , "courier" , monospace;">$OUTPUT</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">.</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">To instantiate my plugin's renderer, I use the </span><span style="font-family: "courier new" , "courier" , monospace;">get_renderer</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> helper function. This function is part of the core </span><span style="font-family: "courier new" , "courier" , monospace;">moodle_page</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> class, and can be accessed through the global </span><span style="font-family: "courier new" , "courier" , monospace;">$PAGE</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> object, available to any Moodle script. Like the global </span><span style="font-family: "courier new" , "courier" , monospace;">$OUTPUT</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> object, the global </span><span style="font-family: "courier new" , "courier" , monospace;">$PAGE</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> object is instantiated at the start of every Moodle script through the normal setup functions. If a script specifically includes the main </span><span style="font-family: "courier new" , "courier" , monospace;">config.php</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> file, it will already have direct access to these global objects. If not, it will need to specifically include them with a PHP </span><span style="font-family: "courier new" , "courier" , monospace;">global</span><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"> statement.</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="background-color: #fefdfa; font-size: 13px;"><span style="font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif;">To get a renderer object for my plugin, I need to add the line:</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> $output = $PAGE->get_renderer('mod_questionnaire');</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"><br /></span></span></span>
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">This function is responsible for locating my renderer and returning an instantiated object of my renderer's class. This is the function that looks for a class named </span><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">mod_questionnaire_renderer</span><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"> in the plugin root directory, or the </span><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">classes/</span><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"> subdirectory, or one that can be autoloaded from the namespace </span><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">mod_questionnaire\output</span><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"> namespace named </span><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">renderer</span><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">.</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"><br /></span></span></span>
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">Once I have this, anywhere I would have used </span><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">$OUTPUT</span><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">, I will now use the plugin's </span><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">$output</span><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"> instead.</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;"><br /></span></span></span>
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">So, I change:</span></span></span><br />
<span style="color: #333333;"><span style="background-color: #fefdfa;"><span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> e</span></span></span><span style="background-color: #fefdfa; color: #333333; font-size: 13px;"><span style="font-family: "courier new" , "courier" , monospace;">cho $OUTPUT->header();</span></span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">to</span><br />
<span style="background-color: #fefdfa; color: #333333; font-size: 13px;"><span style="font-family: "courier new" , "courier" , monospace;"> echo $output->header();</span></span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">And:</span><br />
<span style="background-color: #fefdfa; color: #333333; font-size: 13px;"><span style="font-family: "courier new" , "courier" , monospace;"> echo $OUTPUT->footer();</span></span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px;">to:</span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> echo $output->footer();</span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"><br /></span>
<span style="background-color: #fefdfa; color: #333333; font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">Note that the renderer I created does not specifically contain the </span><span style="font-family: "courier new" , "courier" , monospace;">header()</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> or </span><span style="font-family: "courier new" , "courier" , monospace;">footer()</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> functions. These are inherited from the parent class, </span></span><span style="color: #333333;"><span style="font-family: "courier new" , "courier" , monospace; font-size: 13px;">plugin_renderer_base</span><span style="font-family: "arial" , "helvetica" , sans-serif;"><span style="font-size: 13px;">, and are used as defined there. If I want to change what the output of these functions are, I can extend or replace them in my renderer. But, these are important functions that do much of the heavy lifting for page setup and completion, so it's best to just use the core methods.</span></span></span><br />
<span style="color: #333333; font-family: "arial" , "helvetica" , sans-serif;"><span style="font-size: 13px;"><br /></span></span>
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">The other output my index script currently does is create an </span><span style="font-family: "courier new" , "courier" , monospace;">html_table</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> object, set it up, and output it using the </span><span style="font-family: "courier new" , "courier" , monospace;">html_writer</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> helper class. Since my renderer now takes care of that, I'm going to remove all of those bits and pass the necessary data to the renderer instead. That actual content of the page will be output with the statement:</span></span></span><br />
<span style="color: #333333; font-family: "courier new" , "courier" , monospace;"><span style="font-size: 13px;"> echo $output->render_index($headings, $align, $content);</span></span><br />
<span style="color: #333333; font-family: "arial" , "helvetica" , sans-serif;"><span style="font-size: 13px;"><br /></span></span>
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">The function </span><span style="font-family: "courier new" , "courier" , monospace;">render_index</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> is the one new function I did create, and it is responsible for returning the complete page's HTML to the caller.</span></span></span><br />
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">So, using my renderer, my index page code to perform the page output, is really handled by four statements:</span></span></span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> $output = $PAGE->get_renderer('mod_questionnaire');</span><span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> </span><br />
<span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> echo $output->header();</span><br />
<span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> echo $output->render_index($headings, $align, $content);</span><br />
<span style="color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;"> </span><span style="background-color: #fefdfa; color: #333333; font-family: "courier new" , "courier" , monospace; font-size: 13px;">echo $output->footer();</span><br />
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">The rest of the script is really there to define the three variables passed to the </span><span style="font-family: "courier new" , "courier" , monospace;">render_index</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> function.</span></span></span><br />
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">To see the changes made, the original </span><span style="font-family: "courier new" , "courier" , monospace;">index.php</span><span style="font-family: "arial" , "helvetica" , sans-serif;"> file (before renderers) can be found <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/STARTINGPOINT/index.php">here</a>. The modified version using renderers can be found <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/JUSTRENDERERANDNS/index.php">here</a>. The diff comparison can be viewed <a href="https://github.com/mchurchward/moodle-mod_questionnaire/commit/fd69fc1ccd9735c86283dc9bfe5eb0ede4341d8f#diff-828e0013b8f3bc1bb22b4f57172b019d">here</a>.</span></span></span><br />
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;"><br /></span></span></span>
<span style="color: #333333;"><span style="font-size: 13px;"><span style="font-family: "arial" , "helvetica" , sans-serif;">In the <a href="http://tandl.churchward.ca/2017/01/part-3-setting-up-templates_59.html">next post</a>, I will add template usage to the renderer solution.</span></span></span><div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymoushttp://www.blogger.com/profile/03484924246815879122noreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-26089168669797621042017-01-05T14:48:00.001-05:002017-01-09T09:40:36.394-05:00Part 1 - Setting up the RendererIn part one of the "Adding renderers and templates to your Moodle plugin" series, I will present how to setup and add a renderer file to your plugin.<br />
<br />
So let's begin adding renderers to a plugin. To make things easier, I have set up a fork of questionnaire in my github account and created a branch to work in. If you wish to work along, it is located at <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/BUILD_RENDERERS">https://github.com/mchurchward/moodle-mod_questionnaire/tree/BUILD_RENDERERS</a>. I have added tags to the various work points to see the progression, and I will highlight them at appropriate points.<br />
<br />
The code I will begin with is found at <a href="https://github.com/mchurchward/moodle-mod_questionnaire/tree/STARTINGPOINT">https://github.com/mchurchward/moodle-mod_questionnaire/tree/STARTINGPOINT</a>.<br />
<br />
As I mentioned in the previous post, I am going to start by modifying the <span style="font-family: "courier new" , "courier" , monospace;">index.php</span> file to use renderers rather than inline display code. To do this, I will need to create a renderer file.<br />
<br />
To create a renderer, I need to create a file called <span style="font-family: "courier new" , "courier" , monospace;">renderer.php</span> for my plugin, that contains a class that extends the core <span style="font-family: "courier new" , "courier" , monospace;">plugin_renderer_base</span> class. My class will contain functions with names that define the output I wish to render and/or overrides to existing core render output functions. For this example, since I am rendering the index page, I will create a function called <span style="font-family: "courier new" , "courier" , monospace;">render_index</span>.<br />
<br />
The location of the file can be in one of two places in my plugin's file structure. The most common place currently is in the root of the plugin directory. At the time of this writing, most of the core module plugins use the root of their directory to hold this file (<span style="font-family: "courier new" , "courier" , monospace;">mod/lti</span> is the only one that doesn't). The other location is within the subdirectory <span style="font-family: "courier new" , "courier" , monospace;">classes/output/</span>.<br />
<br />
My <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/STARTINGPOINT/index.php" target="_blank">index page at the start</a> is essentially a table, created and output using the core Moodle <span style="font-family: "courier new" , "courier" , monospace;">html_table</span> class from the <span style="font-family: "courier new" , "courier" , monospace;">/lib/outputcomponents.php</span> file. For now, I am just going to move the usage of the <span style="font-family: "courier new" , "courier" , monospace;">html_table</span> class to my renderer. So I will create the file <span style="font-family: "courier new" , "courier" , monospace;">renderer.php</span> with the class definition <span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire_renderer</span> and the function <span style="font-family: "courier new" , "courier" , monospace;">render_index()</span>in the root of my plugin.<br />
<br />
Note the class name I have used: <span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire_renderer</span> . This is so Moodle's rendering system can find it. When I use the rendering functions I define, Moodle will look for a class definition named "<span style="font-family: "courier new" , "courier" , monospace;">plugintype]_[pluginname]_renderer</span>" using Moodle's standard <a href="https://docs.moodle.org/dev/Frankenstyle" target="_blank">frankenstyle</a> plugin naming convention.<br />
<br />
The <span style="font-family: "courier new" , "courier" , monospace;">render_index</span> function will take three arguments to create the output table. The heading for the heading row, the alignment for all columns and the actual row/column data. The important code for this file looks like this (you can see the whole file <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/JUSTRENDERER/renderer.php" target="_blank">here</a>):<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">class mod_questionnaire_renderer extends plugin_renderer_base {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">/**</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * Renders the HTML for the index page.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param array $headings Headings for the display columns.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param array $align Alignment for each column.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param array $data All of the table data.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @return string</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> */</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function render_index($headings, $align, $data) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table = new html_table();</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table->head = $headings;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table->align = $align;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table->data = $data;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> return html_writer::table($table);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">}</span><br />
<br />
Since the purpose of the renderer functions are to return the display code (HTML) to be output, all I have done for now is moved the output from the <span style="font-family: "courier new" , "courier" , monospace;">html_table</span> class to here. Not terribly interesting, but it still provides the advantages of a renderer, such as overriding it with themes.<br />
<br />
As I mentioned previously, the class naming convention allows Moodle's rendering system to locate my plugin's renderer. The mechanism for this is:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> $myrenderer = $PAGE->get_renderer('mod_questionnaire');</span><br />
<br />
When I modify the <span style="font-family: "courier new" , "courier" , monospace;">index.php</span> file, a line like that will be what I will use to define the renderer object I will use to generate my output.<br />
<br />
Before I move on, I want to change my renderer to use <a href="https://docs.moodle.org/dev/Automatic_class_loading#Namespaces" target="_blank">namespaces</a> and class autoloading. Namespaces allow for my classes to be more specifically identified, allowing Moodle's autoloading system to find them much quicker. They also allow me to have class names identical to other class names in Moodle without causing a coding error. This is because the full class name also contains the namespace to identify it.<br />
<br />
The <span style="font-family: "courier new" , "courier" , monospace;">classes/</span> subdirectory is a new structure released in 2.6. It provides a standard mechanism to provide the class definitions your plugin requires, and can contain any subdirectory structure you require. It also allows Moodle to use <a href="https://docs.moodle.org/dev/Automatic_class_loading" target="_blank">automatic class loading</a>, meaning that files containing class definitions can be discovered by other scripts rather than having to be specifically included in the script. When autoloading is combined with namespace usage (more on that in a bit), the code may perform better and will be easier to maintain with fewer backward compatibility problems.<br />
<br />
Moodle's renderer autoloading system automatically looks in both the <span style="font-family: "courier new" , "courier" , monospace;">classes/output/</span> space and the plugin root for plugin renderers. But, other class definitions will only be looked for in the <span style="font-family: "courier new" , "courier" , monospace;">classes/</span> structure.<br />
<br />
To use autoloading and namespaces, I will move my <span style="font-family: "courier new" , "courier" , monospace;">renderer.php</span> file from the plugin root to the <span style="font-family: "courier new" , "courier" , monospace;">classes/output/</span> directory.<br />
<br />
In the newly moved file, before the first line of code, I need to put in my namespace definition. Like the frankenstyle name of my class, a namespace consists of the <span style="font-family: "courier new" , "courier" , monospace;">[plugintype]_[pluginname]</span>, followed by an optional functional name for your space. When I use a namespace, the file must be located in my <span style="font-family: "courier new" , "courier" , monospace;">classes/</span> subdirectory in order for the autoloading system to find it. Since I want to use namespaces, this is another reason why I located my renderer in <span style="font-family: "courier new" , "courier" , monospace;">classes/output</span> rather than my plugin root. Moodle expects to find the renderer classes in the "output" namespace for a plugin, so my namespace declaration will look like this:<br />
<span style="font-family: "courier new" , "courier" , monospace;"> namespace mod_questionnaire\output;</span><br />
<br />
Notice that the "output" portion corresponds with the "output" subdirectory of "classes". This is how namespaces work. Every part of a namespace declaration in Moodle separated by "\" is expected to be a subdirectory in the "<span style="font-family: "courier new" , "courier" , monospace;">classes/</span>" structure.<br />
<br />
Defining this namespace further allows me to change the name of my class. Currently, the class is named <span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire_renderer</span>. Because I have now declared to be a part of the <span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire</span> namespace, I no longer need the "mod_questionnaire" portion. My class must now be named just <span style="font-family: "courier new" , "courier" , monospace;">renderer</span>.<br />
<br />
With these changes, the code in my renderer now looks like this (full file can be found <a href="https://github.com/mchurchward/moodle-mod_questionnaire/blob/JUSTRENDERERANDNS/classes/output/renderer.php" target="_blank">here</a>):<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;">namespace mod_questionnaire\output;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;">class renderer extends \plugin_renderer_base {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> /**</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * Renders the HTML for the index page.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param array $headings Headings for the display columns.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param array $align Alignment for each column.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @param array $data All of the table data.</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> * @return string</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> */</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> public function render_index($headings, $align, $data) {</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table = new \html_table();</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table->head = $headings;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table->align = $align;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> $table->data = $data;</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"><br /></span>
<span style="font-family: "courier new" , "courier" , monospace;"> return \html_writer::table($table);</span><br />
<span style="font-family: "courier new" , "courier" , monospace;"> }</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">}</span><br />
<br />
Note one other important, but subtle change. There is a backslash ("\") in front of the <span style="font-family: "courier new" , "courier" , monospace;">plugin_renderer_base</span>, the <span style="font-family: "courier new" , "courier" , monospace;">html_table</span> and the <span style="font-family: "courier new" , "courier" , monospace;">html_writer</span> statements. This defines the namespace for those constructs as the root (core) namespace. This is necessary, since I have specified that the code in this file is in the <span style="font-family: "courier new" , "courier" , monospace;">mod_questionnaire\output</span> namespace. Unless otherwise specified, all definitions will be expected to be in that same namespace. Since those items are contained in other namespaces (namely Moodle core or "\"), I need to specifically identify where they can be found for my code to work.<br />
<br />
In my <a href="http://tandl.churchward.ca/2017/01/part-2-using-renderer_9.html">next post</a>, I will modify the <span style="font-family: "courier new" , "courier" , monospace;">index.php</span> file to use the newly defined renderer.<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymousnoreply@blogger.com1tag:blogger.com,1999:blog-7094986855457289958.post-61402257291191185772017-01-04T15:01:00.000-05:002017-01-06T08:47:23.981-05:00Adding renderers and templates to your Moodle plugin - IntroductionIf, like me, you're maintaining Moodle plugins that have been around since the early 1.x days, you are constantly planning to modernize and upgrade them. Not doing so, increases the <a href="https://en.wikipedia.org/wiki/Technical_debt" target="_blank">technical debt</a> you will have to endure.<br />
<br />
Moodle has been constantly updating its architecture and API's and has completely new ways of dealing with output so that information display can be more easily adapted to changing situations and experiences. The rendering and templating systems are such solutions.<br />
<br />
In the old days, our plugins simply mixed the display code with the business logic and output to the screen in any script. There were (and are) core library functions to put output to the screen, such as the "mforms" library, but generally these were used within the plugins logic.<br />
<br />
In Moodle 2.0, <a href="https://docs.moodle.org/dev/Renderer" target="_blank">renderers</a> were introduced to help separate the logic code from the display code and make it easier for themes and theme designers to have things look the way they want them to.<br />
<br />
In Moodle 2.9, this system was expanded further to take advantage of <a href="https://docs.moodle.org/dev/Templates" target="_blank">templates</a>, using the "Mustache" language. This enabled a much larger separation of the HTML from the PHP/Javascript, allowing display designers to work more directly with what the output will actually look like.<br />
<br />
Going forward, you'll want to be using the Moodle template system with renderers to code your display logic.<br />
<br />
I'm going to refactor my "questionnaire" plugin to use Moodle renderers and templates as part of a major "<a href="https://tracker.moodle.org/browse/CONTRIB-6034" target="_blank">modernize</a>" effort for Moodle 3. This will require some significant work as there is display code scattered throughout the various PHP files. For this post, I am going to simplify the work to become familiar with the rendering and templating systems.<br />
<br />
I will recreate questionnaire's "index.php" function using renderers and templates as my test bed. This file's output is relatively simple and this will be easy to recreate. Additionally, it is not used directly by core Moodle anywhere, so breaking it won't be a problem to my test site.<br />
<br />
My <a href="http://tandl.churchward.ca/2017/01/adding-renderers-and-templates-to-your_5.html">next post</a> will begin the process. If you wish to follow along, get familiar with Moodle's <a href="https://docs.moodle.org/dev/Output_renderers" target="_blank">rendering system</a>, <a href="https://docs.moodle.org/dev/Templates" target="_blank">templating system</a> as well as <a href="https://docs.moodle.org/dev/Automatic_class_loading" target="_blank">autoloading and namespaces</a>.<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymousnoreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-72707380404863394012015-06-03T10:37:00.003-04:002015-06-03T10:37:56.069-04:00UK Moodlemoot 2015 - Thoughts on Community and OpennessTwo weeks ago, I attended the UK Moodlemoot in Dublin. This was around the twelfth Moodlemoot I have participated in, most of which I have actively presented in as well as attended. I always find the Moots’ spirit of community so energizing, and this one was no exception.<br />
<br />
Attending a Moodlemoot gets me back in the community and its sub-communities - developers, educators, users and partners. It reminds me why I do what I do, and makes me happy to be part of the engine that makes Moodle go. A Moot is a reunion of sorts, where I connect with people who I see “virtually” every week, but only physically meet with in geographically far-off places every couple of years. And as a reunion, it makes me review my history.<br />
<br />
I began my Moodle connection in 2003, with my first “moodle.org” post on November 27, 2003 contributing my ideas for “Moodle groups” that was being worked on for Moodle 1.1. That same discussion included three colleagues who went on to start their own Moodle Partner businesses - Sean Keogh, Bryan Williams and Tom Murdock. Eventually, Sean, Bryan and I merged our three Moodle Partner companies into one. Tom Murdock co-founded Moodlerooms, which just recently acquired our UK division. Sean and I were both there at the recent Moot.<br />
<br />
In February 2004, I released my first module - questionnaire. Questionnaire remains one of the top plugin downloads in the Moodle plugin database, usually sitting in second or third spot.<br />
<br />
in June of 2005, I attended my first MoodleMoot at what was then the Governor Dummer Academy near Boston. There I met Martin Dougiamas for the first time in person, as well as Michelle Moore, who I would later work with as well. Both were there at the Moot.<br />
<br />
Over the next decade, I continued Moodle development, formed my own Moodle Partner company, attended and presented many more Moots, attended various developer conferences and hackfests, wrote a book on Moodle plugin development and I believe contributed to Moodle’s success. Some of the developers I’ve attended the developer gatherings with were there as well: Dan Poltawski, David Mudrak, Davo Smith, Bas Brands among others.<br />
<br />
Now, arriving at Dublin for the 2015 UK Moot, I still found myself amazed by the spirit of collaboration this experiment of Martin Dougiamas has created. I was there to present our latest releases of plug-ins designed to help Office365 users integrate with Moodle, and front a workshop to help learn how to setup and use these plugins. The workshop was attended by new faces as well as known collaborators. When I ran into trouble, someone in the room was there to help me out. I received valuable feedback that will go back into the plugin development.<br />
<br />
Likewise the various sessions demonstrated the Moodle community energy. No presenter felt alone, and I think all left with as many new ideas as they provided. You just know, more good ideas will end up available to us all as a result. I personally, have already contacted a couple of the presenters to see if I can get more involved in their projects.<br />
<br />
And of course the impromptu meetups in the social times generated many great discussions on Moodle, learning and the industry in general. Many of these went well into the night and well into the Guinness kegs. My personal discussions involved educators, Moodle users and administrators, developers and other partners. I found all of these talks led to positive ideas of how we can improve Moodle, learning and our “product”.<br />
<br />
My hats off to Gavin Henrick, another colleague and past co-worker, for organizing a successful Moot that so celebrated and promoted the spirit of openness the Moodle community brings. I love being a part of this community and its innovative energy. Looking forward to the next one.<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymousnoreply@blogger.com0tag:blogger.com,1999:blog-7094986855457289958.post-1756869166664011622015-04-07T17:03:00.000-04:002015-04-07T17:03:01.123-04:00Office 365 Moodle Integration Project - New and ImprovedSince we released the O365 integration plug-ins back in January, there has been a lot of community pick-up and feedback. We have responded with a number of improvements. This article will focus on the most noticeable surrounding the O365 single-sign-on and account connection.<br />
<br />
The original release of the O365 plug-ins required users who wanted to use the O365 OneDrive and Outlook Calendar Moodle integrations to change their Moodle account authentication scheme to the OpenID Connect using Office 365’s AAD system. Any new Moodle account had to be created and managed from the Office 365 AAD system as well. To login, a user had to use the separate “OpenID Connect” login button on the Moodle login page.<br />
<br />
This system worked well for new Moodle installations, where the user management system had not been established, and was therefore easy to select the OpenID Connect/AAD authentication system for their user system. But for established Moodle sites, where other user management and authentication systems were well established, this proved to be a barrier to using the O365 integrations.<br />
<br />
Further, some Moodle sites that did want to use the OpenID Connect/AAD system for Moodle authentication, didn’t want their users to look for a separate login button on the Moodle login page, but to continue to use the standard Moodle login form.<br />
<br />
I’ll deal with the second problem first.<br />
<br />
As originally released, to login via O365, you had to use a separate button on the login page, like below:<br />
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-sJ5Em432O-Y/VSQ6heojrLI/AAAAAAAABxk/foVIQtXtZDE/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.11.30%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-sJ5Em432O-Y/VSQ6heojrLI/AAAAAAAABxk/foVIQtXtZDE/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.11.30%2BPM.png" height="320" width="290" /></a></div>
<div>
<br /></div>
<div>
However, Moodle is built such that authentication plug-ins can use the standard login form, if they choose to do so. For this case, we built in the ability for Moodle to pass the entered username and password to the OpenID Connect provider, and log the user in to both O365/AAD and Moodle at the same time, in the background. In order to facilitate this, we provided an extra configuration option:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-C5VAKIEap_w/VSQ7XjfPG3I/AAAAAAAABxs/v5asfbtSsTQ/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.17.14%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-C5VAKIEap_w/VSQ7XjfPG3I/AAAAAAAABxs/v5asfbtSsTQ/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.17.14%2BPM.png" height="120" width="640" /></a></div>
<div>
<br /></div>
<div>
As shown, the second radio button turns off the need to use the separate OpenID Connect login button, and allows the standard Moodle login form to function with OpenID Connect. If you select the first radio button, the login will work as it did before, and require the user to login using the external provider.</div>
<div>
<br /></div>
<div>
One other addition we added was the ability to select an icon for use on the login button, or upload a custom icon for the same use. This allows the OpenID Connect login button to have an icon more suited to the organization, if they choose not to show the Microsoft Office icon.</div>
<div>
<br /></div>
<div>
For the account connection problem, we approached it in a unique way. As the account connection mechanism existed, sites that wanted their users to take advantage of the O365 integrations required that the Moodle logins be managed from O365/AAD as well. The plug-ins provided an easy mechanism to connect existing Moodle accounts to existing O365 accounts, and then switch those users to the O365/AAD login, but that was a permanent change. And sites with many users, already have well-established user management and login systems that they use with Moodle.</div>
<div>
<br /></div>
<div>
To solve that problem, we provided a mechanism that allows users to either switch to using the O365 login (as before), or connect their current Moodle account to an O365 account and continue to login to Moodle as they did before. This mechanism is contained in the user profile plug-ins accessed from a user's Moodle profile page, as so:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-oM0cDYAnefY/VSQ_Xw2BvcI/AAAAAAAABx4/RCH9Lsq7ZwU/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.34.18%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-oM0cDYAnefY/VSQ_Xw2BvcI/AAAAAAAABx4/RCH9Lsq7ZwU/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.34.18%2BPM.png" height="300" width="640" /></a></div>
<div>
<br /></div>
<div>
In this shot, you can see two Office365 links: </div>
<div>
<ul>
<li>Office365 Connection: You are not connected to Office 365. <u><span style="color: #0b5394;">Connect to Office 365</span></u></li>
<li>Office365: You are not using Office365 to log in. <span style="color: #0b5394;"><u>Start using Office365 to log in.</u></span></li>
</ul>
</div>
<div>
Either of these links takes you to the new O365 management page, which provides links to each function as so:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-47mPkfKhecc/VSRAmrYvWFI/AAAAAAAAByE/fsiNnOzhYSE/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.39.31%2BPM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-47mPkfKhecc/VSRAmrYvWFI/AAAAAAAAByE/fsiNnOzhYSE/s1600/Screen%2BShot%2B2015-04-07%2Bat%2B4.39.31%2BPM.png" height="188" width="400" /></a></div>
<div>
<br /></div>
<div>
The first function on this page, "Start using Office365 to log into Moodle", works the same as the first release. If you click that link, you will be redirected to your Office365 login page to login to O365/AAD. If you login with a valid AAD account that has not already been connected to a Moodle account, your Moodle account will then be reconfigured to use AAD as the authentication scheme. Once that has been done, you will always need to login to Moodle using the AAD account information.</div>
<div>
<br /></div>
<div>
The second function in this page, "Connect to Office365", is the new function. If you click that link, you will be likewise redirected to your Office365 login page. If you login with a valid AAD account that has not already been connected to a Moodle account, your Moodle account will be configured to be connected to that O365 account. What that means, is that you will continue to login to Moodle in the same way you always have, but when you access any of the O365 Moodle integrations, your O365 account will automatically be used. You won't need to login twice once the connection has been made.</div>
<div>
<br /></div>
<div>
This second feature was one that was most asked for. This feature allows Moodle sites that cannot change their Moodle authentication schemes to still take advantage of the full O365 integrations.</div>
<div>
<br /></div>
<div>
One feature that has also remained is the automatic account creation. If a user that does not exist yet in Moodle, logs into a Moodle site that has the OpenID Connect O365/AAD authentication plug-in enabled, and the account information they use is a valid AAD account, they will be logged in and a new Moodle account created for them.</div>
<div>
<br /></div>
<div>
We have a number of other improvements and new features coming in future releases. Look to future blog posts describing these.</div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymousnoreply@blogger.com1tag:blogger.com,1999:blog-7094986855457289958.post-75672080969743185042015-01-19T10:59:00.000-05:002015-01-19T10:59:36.172-05:00Office 365 Moodle Integration Project - Released!This will be a short post, just to let everyone know that our first release of the Office 365 plug-ins are now available to the community.<br />
<br />
Earlier today, Jean Paoli, president of MS Open Tech <a href="http://microsoft-news.com/microsoft-announces-integration-between-moodle-and-office-365/" target="_blank">announced the official release of the code</a> at the <a href="http://www.bettshow.com/" target="_blank">BETT</a> show in London. This announcement coincided with the opening of the Githib repositories to the public and the submissions to the Moodle Plugins database.<br />
<br />
I have posted an <a href="https://moodle.org/mod/forum/discuss.php?d=278647" target="_blank">announcement</a> in the Moodle "General plugins" forum, and more discussion can happen there. There will also be tracker items set up in the Moodle tracker to get feature and improvement requests, and deal with bugs.<br />
<br />
You can get the plugins from the Moodle plugins database as follows:<br />
<br />
<ul>
<li><a href="https://moodle.org/plugins/browse.php?list=set&id=71" target="_blank">OneNote set of plugins</a>.</li>
<li><a href="https://moodle.org/plugins/browse.php?list=set&id=72" target="_blank">Office365 for Business set of plugins</a>.</li>
<li><a href="https://moodle.org/plugins/view/filter_oembed" target="_blank">Oembed filter</a>. It may still be out of date, but will be updated shortly.</li>
</ul>
<div>
The current work for each has been gathered in one Github repository called "<a href="https://github.com/MSOpenTech/o365-moodle" target="_blank">o365-moodle</a>", but each plug-in has a current release repository of its own. The "readme" file in the "o365-moodle" repository has references for each release repository.</div>
<div>
<br /></div>
<div>
Start playing!</div>
<div>
<br /></div>
<div>
If you want any more information on how you can benefit from these developments, contact Remote-Learner at moodleo365@remote-learner.net.</div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymousnoreply@blogger.com29tag:blogger.com,1999:blog-7094986855457289958.post-78063969077788244722015-01-08T11:14:00.002-05:002015-01-08T11:15:41.916-05:00Office 365 Moodle Integration Project - OneDriveOur initial work with Microsoft Open Technologies has progressed, and we now have an integration with OneDrive for Business. In this post, I will describe and provide brief demos of how Moodle and OneDrive for Business can work together.<br />
<div>
<br /></div>
<div>
At its basic level, the OneDrive for Business integration is a repository plug-in. It is built to work closely with the OpenID Connect authentication plug-in that provides the Office 365 account connection. It provides two levels of repositories to every user. It provides access to each user's private files area, and it provides access to a course shared area for every course a user is enrolled in.</div>
<div>
<br /></div>
<div>
I'll start by looking at the personal space. Below is a screen capture of the Moodle file picker using the Office 365 OneDrive repository.</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-b2jidln5kq0/VK6X13mXpGI/AAAAAAAABqY/f7byv8uNjz0/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B9.43.14%2BAM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-b2jidln5kq0/VK6X13mXpGI/AAAAAAAABqY/f7byv8uNjz0/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B9.43.14%2BAM.png" height="258" width="320" /></a></div>
<div>
<br />
What I can see here are two folders in OneDrive. The "My Files" folder is my personal space in the OneDrive for Business application. The "Courses" folder contains subfolders for each of the Moodle courses I have access to. "My Files" displays all of the files and directories I have in my OneDrive for Business folder. The image below shows the contents in the Moodle file picker.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-1UHtJeUT9qg/VK6Z_eHhEbI/AAAAAAAABqk/dts4EP6A57I/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B9.53.22%2BAM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-1UHtJeUT9qg/VK6Z_eHhEbI/AAAAAAAABqk/dts4EP6A57I/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B9.53.22%2BAM.png" height="206" width="320" /></a></div>
<br />
Looking at my OneDrive interface, I see a similar structure as below.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-3u_rFZ_RhZU/VK6acuNkMuI/AAAAAAAABqs/jtZIw2EFNb4/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B9.54.39%2BAM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-3u_rFZ_RhZU/VK6acuNkMuI/AAAAAAAABqs/jtZIw2EFNb4/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B9.54.39%2BAM.png" height="131" width="320" /></a></div>
<br />
Note that the folder structure and contents mirrors what I saw in Moodle. If I add or change the contents of my OneDrive folder, it will likewise be reflected in the Moodle file picker. This is what we expect from a Moodle repository plug-in. :)<br />
<br />
If I add files to my OneDrive, I will see them when I look at "My Files" in the Moodle Office 365 OneDrive "My Files" area. Likewise, if I upload a file from Moodle into the "My Files" area, I will see it in my OneDrive folder. In the Moodle file picker image of "My Files" above, note the "Upload New File" control. Using that, I can select a file from my local machine, and upload it directly into my OneDrive folder via Moodle, where it will be available to both OneDrive and my Moodle files.<br />
<br />
The "Courses" folder also uses my Office 365 account. It provides course-level shared access to files for all of the Moodle courses on the site that I have access to. A separate folder is created for each course, using the Moodle course names. To do this, Office 365 for Business uses Sharepoint, and specifically, Sharepoint sites. Below are two images showing the same course level directory on Moodle and on Sharepoint (OneDrive).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-IHhTvnriC4k/VK6m_FmTPgI/AAAAAAAABrg/tmW6VrMGZoQ/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B10.48.23%2BAM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://4.bp.blogspot.com/-IHhTvnriC4k/VK6m_FmTPgI/AAAAAAAABrg/tmW6VrMGZoQ/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B10.48.23%2BAM.png" height="202" width="320" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-xg2GNNE3HdI/VK6nZdQdu5I/AAAAAAAABro/HOSsCRDbeSs/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B10.50.37%2BAM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-xg2GNNE3HdI/VK6nZdQdu5I/AAAAAAAABro/HOSsCRDbeSs/s1600/Screen%2BShot%2B2015-01-08%2Bat%2B10.50.37%2BAM.png" height="142" width="320" /></a></div>
<br />
The integration uses Moodle capabilities to control access to the files in both Moodle and Sharepoint/OneDrive. By default, the teacher roles in a course are given read and write access to the course folder, while students are given read only. Moodle synchronizes these access permissions to Sharepoint to control it in the Office 365 realm as well.<br />
<br />
Showing the path through Office 365 to the Moodle course files is difficult with screen snaps, so instead, below I have included a small video showing the path through to the course files.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.blogger.com/video.g?token=AD6v5dwDIObY5PivchgEtCFKe9vzpHT9up6ld9ckSVyaLouhY2he43NNS6hRf1Ga-gGDa7R4AEl9obc72Hlj6ZmSuQ' class='b-hbp-video b-uploaded' frameborder='0'></iframe></div>
<br />
We're very close to releasing all of this work to the community at large. And we're very excited to find out how its received. In future posts, I will discuss the technical solutions underneath the hood in more detail.<br />
<br />
If you want any more information on how you can benefit from these developments, contact Remote-Learner at moodleo365@remote-learner.net.</div>
<div class="blogger-post-footer">Mike Churchward is the Executive Director of the POET Group. His charge is to bring together the necessary resources and structures to focus efforts on Open Source projects of interest to POET's members. Contact Mike at mike.churchward@poetgroup.org, or check out the website at http://poetgroup.org/.</div>Anonymousnoreply@blogger.com2