Monday, November 28, 2011

Converting Moodle 1.9 Plug-ins to Moodle 2 - Activity Module Upgrade - Part 2

This is the second part in my Moodle 1.9 to Moodle 2 activity module migration.

When I left off, I had modified the installation and upgrade code using the new XMLDB/DDL changes. Now I'm going to focus on the DML changes.

The most obvious change I need to make throughout the module is changing all DML functions (get_record, set_record, etc.) to be methods of the global $DB object instead of standalone functions. So get_record becomes $DB->get_record, and so on. The other most obvious change is that the 'parameter / value' arguments that used to be passed to these functions are now replaced by an array with 'key => value' pairs instead. There are many other changes I will have to make, but I'll look at those as I come to them.

One of the tools this document points me to, is the 'check_db_syntax' helper script. This script is designed to locate all areas of a plug-in that need database code changed. The instructions are there to retrieve this script and execute. When I run this script on the code, I get a lot of information. I have provided the output below, but I have removed the items involving the backup and restore scripts, since I will deal with backup and restore separately:
Checking the /home/www/moodle.git/mod/stampcoll directory recursively
(executed from custom directory - false positive detection DISABLED!)
- /home/www/moodle.git/mod/stampcoll/mod_form.php: ... OK
- /home/www/moodle.git/mod/stampcoll/styles.php: ... OK
- /home/www/moodle.git/mod/stampcoll/lib.php:
* ERROR found!
- ERROR ( DML ) - line 22 : if ($stamps = get_records_select("stampcoll_stamps", "userid=$user->id AND stampcollid=$stampcoll->id")) {
- ERROR ( DML ) - line 88 : return insert_record("stampcoll", $stampcoll);
- ERROR ( DML ) - line 101 : return update_record('stampcoll', $stampcoll);
- ERROR ( DML ) - line 112 : if (! $stampcoll = get_record("stampcoll", "id", "$id")) {
- ERROR ( DML ) - line 118 : if (! delete_records("stampcoll_stamps", "stampcollid", "$stampcoll->id")) {
- ERROR ( DML ) - line 122 : if (! delete_records("stampcoll", "id", "$stampcoll->id")) {
- ERROR ( DML ) - line 140 : $students = get_records_sql("SELECT DISTINCT u.id, u.id
- ERROR ( OTHER ) - line 141 : FROM {$CFG->prefix}user u,
- ERROR ( OTHER ) - line 142 : {$CFG->prefix}stampcoll_stamps s
- ERROR ( DML ) - line 182 : return get_record("stampcoll", "id", $stampcollid);
- ERROR ( DML ) - line 192 : return get_records("stampcoll_stamps", "stampcollid", $stampcollid, "id");
- ERROR ( DML ) - line 202 : return get_record("stampcoll_stamps", "id", $stampid);
- /home/www/moodle.git/mod/stampcoll/editstamps.php:
* ERROR found!
- ERROR ( DML ) - line 13 : if (! $course = get_record("course", "id", $cm->course)) {
- ERROR ( DML ) - line 81 : if (! $newstamp->id = insert_record("stampcoll_stamps", $newstamp)) {
- ERROR ( DML ) - line 102 : if (! update_record("stampcoll_stamps", $updatedstamp)) {
- ERROR ( DML ) - line 120 : if (! delete_records("stampcoll_stamps", "id", $form->deletestamp)) {
- ERROR ( OTHER ) - line 241 : $sql = 'FROM '.$CFG->prefix.'user AS u '.
- ERROR ( OTHER ) - line 242 : 'LEFT JOIN '.$CFG->prefix.'stampcoll_stamps s ON u.id = s.userid AND s.stampcollid = '.$stampcoll->id.' '.
- ERROR ( DML ) - line 247 : if (($ausers = get_records_sql($select.$sql.$sort, $table->get_page_start(), $table->get_page_size())) !== false) {
- /home/www/moodle.git/mod/stampcoll/version.php: ... OK
- /home/www/moodle.git/mod/stampcoll/view.php:
* ERROR found!
- ERROR ( DML ) - line 14 : if (! $course = get_record("course", "id", $cm->course)) {
- ERROR ( OTHER ) - line 171 : $sql = 'FROM '.$CFG->prefix.'user AS u '.
- ERROR ( OTHER ) - line 172 : 'LEFT JOIN '.$CFG->prefix.'stampcoll_stamps s ON u.id = s.userid AND s.stampcollid = '.$stampcoll->id.' '.
- ERROR ( DML ) - line 181 : if (($ausers = get_records_sql($select.$sql.$sort)) !== false) {
- ERROR ( DML ) - line 184 : if (($ausers = get_records_sql($select.$sql.$sort, $table->get_page_start(), $table->get_page_size())) !== false) {
- /home/www/moodle.git/mod/stampcoll/caps.php: ... OK
- /home/www/moodle.git/mod/stampcoll/tabs.php: ... OK
- /home/www/moodle.git/mod/stampcoll/index.php:
* ERROR found!
- ERROR ( DML ) - line 8 : if (! $course = get_record('course', 'id', $id)) {
- /home/www/moodle.git/mod/stampcoll/db/log.php: ... OK
- /home/www/moodle.git/mod/stampcoll/db/install.xml: ... OK
- /home/www/moodle.git/mod/stampcoll/db/access.php: ... OK
- /home/www/moodle.git/mod/stampcoll/db/upgrade.php:
* ERROR found!
- ERROR ( DML ) - line 29 : if ($collections = get_records('stampcoll', 'publish', '0')) {
- ERROR ( DML ) - line 44 : if ($collections = get_records('stampcoll', 'publish', '2')) {
- ERROR ( DML ) - line 64 : if ($collections = get_records('stampcoll', 'teachercancollect', '1')) {
 This report gives me a pretty good list of code locations I need to fix.

 I will go through each of these and upgrade to the new method. Some examples of changes are, "editstamps.php" line 13, I change:
if (! $course = get_record("course", "id", $cm->course)) {
    error("Course is misconfigured");
}
to:
if (! $course = $DB->get_record("course", array("id" => $cm->course))) {
    error("Course is misconfigured");
}
In this case, I added the $DB-> to the call, and changed the parameters to a "key/value" array.

In "editstamps.php" line 81, I change:
if (! $newstamp->id = insert_record("stampcoll_stamps", $newstamp)) {
    error("Could not save new stamp");
}
to:
if (! $newstamp->id = $DB->insert_record("stampcoll_stamps", $newstamp)) {
    error("Could not save new stamp");
}
In this case, I only added the $DB->, since the rest of the function is the same as 1.9.

In "editstamps.php" line 240-247, I change:
$select = 'SELECT u.id, u.firstname, u.lastname, u.picture, COUNT(s.id) AS count ';
$sql = 'FROM '.$CFG->prefix.'user AS u '.
           'LEFT JOIN '.$CFG->prefix.'stampcoll_stamps s ON u.id = s.userid AND s.stampcollid = '.$stampcoll->id.' '.
           'WHERE '.$where.'u.id IN ('.implode(',', array_keys($users)).') GROUP BY u.id, u.firstname, u.lastname, u.picture ';

$table->pagesize($perpage, count($users));
   
if (($ausers = get_records_sql($select.$sql.$sort, $table->get_page_start(), $table->get_page_size())) !== false) {
to:

$select = "SELECT u.id, u.firstname, u.lastname, u.picture, COUNT(s.id) AS count ";
list($uids, $params) = $DB->get_in_or_equal(array_keys($users));

$params['stampcollid'] = $stampcoll->id;
$sql    = "FROM {user} AS u ".
          "LEFT JOIN {stampcoll_stamps} s ON u.id = s.userid AND s.stampcollid = :stampcollid ".
          "WHERE $where u.id $uids ".
          "GROUP BY u.id, u.firstname, u.lastname, u.picture ";

$table->pagesize($perpage, count($users));
   
$ausers = $DB->get_records_sql($select.$sql.$sort, $params, $table->get_page_start(), $table->get_page_size());
There are a number of DML changes here. First, I have added the standard $DB-> part. Next, I have removed the $CFG->prefix portions and instead enclosed the table names in "{}".

I also need to replace the PHP variables in the query with DML parameters. This query has two that I will deal with: $stampcoll->id and $users. The $stampcoll->id I replace with a named placeholder called ":stampcollid". The value of $stampcoll->id needs to be added to an array called $params, that I will pass to the DML function. The $users variable is a special case, because it needs to potentially be used in an "IN (...)" SQL statement.

In the old version, I created the necessary SQL using the code:
'u.id IN ('.implode(',', array_keys($users)).')
In the new version, I will use a new function created for this type of SQL statement, $DB->get_in_or_equal. This function takes an array and returns the proper SQL for "IN" if needed, or an "=" if there is only one value to worry about. My new code replaces the old code with this:
list($uids, $params) = $DB->get_in_or_equal(array_keys($users));
This statement returns the proper SQL structure in $uids, and creates the placeholder value in the $params variable. I do this before loading the 'stampcollid' placeholder value, as it creates the $params array that I need. After the array is created, I load the 'stampcollid' parameter value with:
$params['stampcollid'] = $stampcoll->id;
In the new SQL statement, you can see the use of ":stampcollid" and "$uids".


Finally, I modify the get_records_sql line significantly. I add the standard $DB->, I add $params as the second parameter and I completely remove the "if" statement from around the call. The reason I remove the "if" statement is that the DML records functions now always return an array. If no records are found, the array is empty. This means that the following "for" block will work no matter what is returned.


I also change the other two lines with database code and save "editstamps.php".

I make similar changes in "view.php", "index.php", "lib.php" and "upgrade.php" as listed in the "check_db_syntax" script output above. One thing to note, is that when I make a change in a function, like in "lib.php", I also add the code declaration "global $DB;". Without this, $DB is undefined in a function.


After making all the changes, I rerun the syntax script. The only changes noted are in the backup and restore code, which I will deal with later.


This code might run, but there are other changes I need to make still. So for now, I'll just leave it as it is.

Tuesday, November 22, 2011

Converting Moodle 1.9 Plug-ins to Moodle 2 - Activity Module Upgrade - Part 1

Continuing on with my Moodle 1.9 to Moodle 2 code migration series, I next want to tackle an activity module. I took a look around the "contrib" areas for a suitable module that needed attention. Alas, I could not find one that was simple enough to do in this blog and hadn't already been migrated. But, I have chosen to do David Mudrack's Stamp Collection module. I have spoken to David, and he is in the process of migrating this, so my version won't be official. Still, it is a good module to migrate as it is straightforward and will allow me to demonstrate the key areas that need to be changed.

I have created a repository for the code, forked from David's. The MOODLE_19_STABLE branch contains the code that will be modified. The MOODLE_21_STABLE branch contains my changes as I migrate the module. Start with the MOODLE_19_STABLE branch if you want to follow along.

When I did the block migration I attempted to install the block with the old code first. With an activity module, I could do this, but I already know with the amount of changes necessary, it will fail. And because of the way Moodle works, as long as that old code is there, I will not be able to get past the upgrade screen as an administrator. So instead, I am going to try and create a methodical upgrade process here.

I'm going to start by referring to the "Migrating contrib code to 2.0" document. This document is for exactly what I'm doing.

Now, here I will diverge for the first time (yes, already). One of the key things missing here is the new language string requirements.

In Moodle 1.9, all plug-ins had language subdirectories with the suffix "_utf8" in their name. In Moodle 2, this suffix is removed. So one of the first things I need to do is rename all of my language subdirectories in this fashion. For example, I rename my '/mod/stampcoll/lang/en_utf8' directory to '/mod/stampcoll/lang/en'. The other key change is to add two new strings to my language file: 'pluginname' and 'pluginadministration'. I add the following to my '/mod/stampcoll/lang/en/stampcoll.php' file:
$string['pluginadministration'] = 'Stamp collections administration';
$string['pluginname'] = 'Stamp collections';
There are some other key changes that I will have to make to my language files, but I will put that off until later.

Back to the migration document, the first thing it notes is that I need a 'requires' statement in my version file. Without this, my module will not pass the installation stage.

Looking at my 'version.php' file, I specifically change:
$module->requires = 2007101508;  // Requires this Moodle version - 1.9 Beta 4
to:
$module->requires = 2010080300;  // Requires this Moodle version
I also upgrade the version of the module to reflect that it is new. For now, I have selected the same version number as the "requires".

Next, the document refers to the database layer. There are many changes in Moodle 2 that impact database functions. I'm going to focus on the XMLDB/DDL changes first, since they impact the actual installation and upgrading of the module.

I have opened up the "db/install.xml" file in a text editor. At the bottom of that file, there is XML code framed in <STATEMENTS> tags. In 1.9, this section was used to execute any specific data statements that needed to be executed after the data tables were set up. Primarily, this section contained records to enter into the Moodle "log_display" table. In Moodle 2, log display entries go into a separate file, "db/log.php". If there are any other data statements required for the module, they now belong in "db/install.php".

In my "db/install.xml" file, I have:
<STATEMENTS>
  <STATEMENT NAME="insert log_display" TYPE="insert" TABLE="log_display" COMMENT="Initial insert of records on table log_display">
    <SENTENCES>
      <SENTENCE TEXT="(module, action, mtable, field) VALUES ('stampcoll', 'view', 'stampcoll', 'name')" />
      <SENTENCE TEXT="(module, action, mtable, field) VALUES ('stampcoll', 'update', 'stampcoll', 'name')" />
      <SENTENCE TEXT="(module, action, mtable, field) VALUES ('stampcoll', 'add', 'stampcoll', 'name')" />
      <SENTENCE TEXT="(module, action, mtable, field) VALUES ('stampcoll', 'update stamp', 'user', 'concat(firstname, \' \', lastname)')" />
      <SENTENCE TEXT="(module, action, mtable, field) VALUES ('stampcoll', 'delete stamp', 'user', 'concat(firstname, \' \', lastname)')" />
    </SENTENCES>
  </STATEMENT>
</STATEMENTS>
I remove all of these statements from the file, and create a new "db/log.php" file containing:
$logs = array(
    array('module'=>'stampcoll', 'action'=>'view', 'mtable'=>'stampcoll', 'field'=>'name'),
    array('module'=>'stampcoll', 'action'=>'update', 'mtable'=>'stampcoll', 'field'=>'name'),
    array('module'=>'stampcoll', 'action'=>'add', 'mtable'=>'stampcoll', 'field'=>'name'),
    array('module'=>'stampcoll', 'action'=>'update stamp', 'mtable'=>'user', 'field'=>'concat(firstname, \' \', lastname)'),
    array('module'=>'stampcoll', 'action'=>'delete stamp', 'mtable'=>'user', 'field'=>'concat(firstname, \' \', lastname)'),
    );
There are no other statements I need to worry about, so I don't need a "db/install.php" file.

Next, I need to remove all "ENUM" statements from the XML. "ENUM" is no longer a valid attribute in Moodle, so the presence of these statements will be flagged as a warning. I could manually remove them from the "install.xml" file right now, but I'm going to leave them. Moodle provides me a simpler method using the XMLDB editor that I will use later.

Finally, I need to make some changes to the existing "db/upgrade.php" script. As documented, there are many changes to be made, most of them straightforward. The summary of what I chang is:
  • Change the $db global declaration to $DB.
  • Add the line "$dbman = $DB->get_manager();".
  • Change all of the "XMLDB" functions to "xmldb_" functions.
  • Change all of the "setAttributes" functions to "set_attributes", removing the sixth and seventh parameters in the few places they are provided.
  • Add $dbman-> to the DDL functions.
  • Remove the use of $result from the DDL functions.
  • Remove all other uses of $result and set the return value to "true".
  • Add upgrade_mod_savepoint calls at the end of each upgrade block.
When completed, I have a "mostly" working upgrade script. There are some changes I will need to make at the start of this script, where DML function calls are used. This will not impact a fresh install though, so I will do that when I do the main DML changes later.

There seems to be one other change I need to make, that I cannot find documented anywhere. In "db/access.php", the variable name $mod_stampcoll_capabilities needs to be changed to $capabilities.
    These should be all the changes I need to have the module install correctly in Moodle 2. Time to give it a shot! Visiting the site notifications page, offers me the option to install the module. I hit the "Upgrade" button, and it installs without errors or warnings.

    Now, there is one more thing I'm going to fix before leaving the installation code. Remember that I noted that "ENUM" statements needed to be removed from the "install.xml" file? Now we're going to use the Moodle tool to do this.

    I select the "XMLDB editor" option from the "Site administration / Development" menu. From there, I look for the "mod/stampcoll/db" entry and click the "Load" link. If I click the "XML" link now, I will see the "ENUM" statements still present. Instead, I click the "Edit" link. This brings me to a screen like the image below. Clicking either of the "XML" links shows me XML without the "ENUM" statements.


    Now, at worst case, you can cut the XML from this display and paste it back into your XML file. At best, you can set your directory permissions so that the application can write to it, and save the changes right from there. Both will work fine, and both will result in XML with the "ENUM" statements gone, which is what Moodle 2 wants.

    Just to make sure my XML is okay, I un-installed my Stamp Collection module, and then re-installed it. Everything worked fine.

    That will be all I do for this part. Next, we will look at the other changes to make the module functional.

    Wednesday, November 16, 2011

    Converting M1.9 Plug-ins to M2 - Block Part 7 - Cleaning Up

    This is part eight of my series concerning porting Moodle 1.9 code to Moodle 2.

    In part four, I ran into an issue where the Ajax script was spitting out warning messages, specifically:
    Coding problem: $PAGE->context was not set. You may have forgotten to call require_login() or $PAGE->set_context(). The page may not display correctly as a result
    I resolved this by adding a require_login and set_context function calls.

    Tim Hunt suggested in the comments that this might be unnecessary if resolved by setting the constant AJAX_SCRIPT to "true". Tim also provided me an example in the Moodle repo to look at.

    I have tried this. I added the line define('AJAX_SCRIPT', true) to the top of my script, and removed the $PAGE->set_context($block->context) line. Unfortunately this caused the warning to come back, leaving me to think that I still need to have the set_context function to complete the rendering function. I left the require_login there as it is more appropriate and secure for that function to be there.

    That said, there are probably good reasons to leave that AJAX_SCRIPT constant in there, so I have looked to see what it does. From the code I have looked at, and some of the issues in the tracker, I believe that setting this constant should prevent some potential problems by setting the page renderer appropriately. This should prevent unnecessary output from normal HTML page output, and package exceptions differently. To be safe, I will leave it there even though it did not solve the problem I hoped it would.

    In part six, I added a global configuration to the block to so that the defaults for instance configurations could be specified. In my original design, I modified the specialization function so that instead of using hard-coded defaults for the instance settings, it checked for a global default setting first. This meant that for each instance setting, one database call was made. An example of what I have is this:
    if(empty($this->config->search_string)) {
        if (($defaultsearch = get_config('block_twitter_search', 'defaultsearch')) === false){
            $defaultsearch = '#moodle';
        }
        $this->config->search_string = $defaultsearch;
    }
    This code is repeated three times; one for each configuration variable. Each one of the calls to get_config is one database access.

    In the comments for this post, Tim Hunt pointed out that get_config can be called without the second argument. When the function is used without that argument, it returns an object with all of the global configuration variables for the plug-in as variables (properties) of that object. Thus, I can get all of the configuration variables with just one database access and reduce the load on the system.

    So, I will create a new function called get_global_config as follows:
    function get_global_config() {
        if (!isset($this->globalconfig)) {
            if (($this->globalconfig = get_config('block_twitter_search')) == null) {
                $this->globalconfig = new stdClass;
                $this->globalconfig->defaultsearch = '#moodle';
                $this->globalconfig->defaultnumtweets = 10;
                $this->globalconfig->defaultpolltime = 30000;
            }
        }
        return $this->globalconfig;
    }
    This function creates a new variable within my block containing the global settings. If there are no global settings, it uses hard-coded defaults. It also returns those settings in case a calling function wishes to use what it set that way. I will use this function to set the instance configuration variables if they have not been specified for the instance in my specialization function:
    function specialization() {
        $this->get_global_config();

        if(!isset($this->config->search_string)) {
            $this->config->search_string = $this->globalconfig->defaultsearch;
        }
        if(!isset($this->config->no_tweets)){
            $this->config->no_tweets = $this->globalconfig->defaultnumtweets;
        }
        if(!isset($this->config->polltime)){
            $this->config->polltime = $this->globalconfig->defaultpolltime;
        }
    }
    By doing things this way, I have reduced the database accesses required for each block instance by two thirds. You may also notice that I changed the use of "empty" statements to "!isset" statements. The reason for this is that empty is true for many conditions including blank and zero values. isset is only true if the variable actually exists. The polltime setting is supposed to be allowed to be zero, so using empty would not have allowed that.

    When I did some further testing, including uninstalling the block completely from my test site, I discovered that the global configuration values were not removed from the 'config_plugins' table. This means that there are remnants of the block left in the database after I have deleted the block. Ideally, Moodle should handle this with the uninstall process automatically, but after looking through the code in detail, I have determined that it does not. I have logged an issue with the Moodle team in the Moodle Tracker describing this problem. You can see it under MDL-30327.

    Until that bug is fixed, if I can solve the problem, I should. So, I will add a new method to my block called before_delete. This function is part of the blocks API, and is called by the system when the block is being uninstalled but before the uninstall operation completes. I will use this function to delete the global configuration data specifically from the block code when it is being deleted. My new function looks like this:
    function before_delete() {
        unset_all_config_for_plugin('block_twitter_search');
    }
    With the addition of this function, my block now completely cleans up after itself when it is removed.

    In part 6, when I was creating the configuration code, I noted that the global settings link for the block was visible in the menu tree, but would not be visible in the main block management page unless there was a has_config method defined that returned "true". In the comments to this post, Tim Hunt suggested that this may in fact be a bug, rather than intended functionality. I have therefore added MDL-30332 to the tracker for the Moodle team to look over.


    There have been requests to make the code I am playing with available via Git. To that end, I have set up a public repository called "moodleaddons" at Github. There are two branches available there: MOODLE_19_STABLE which contains the code I started with, and MOODLE_21_STABLE which contains the code as it is now. Feel free to grab that code and play with it anyway you'd like. If you are not a Git user, there is a link to grab a zip of the code as well.

    Lastly, Kevin Hughes, the original code author has now made his Moodle 2 version available at the Moodle downloads site. He has commented that he will look at the changes done in this series for possible inclusion into his release.

    I am going to put this code away for now, and will look for a good Moodle module to focus on next. Please let me know if you have any suggestions for code that I can look at.

    Tuesday, November 8, 2011

    Converting M1.9 Plug-ins to M2 - Block Part 6 - Global Config

    This is part seven of my series concerning porting Moodle 1.9 code to Moodle 2.

    I have decided that the block needs to allow its default settings to be configured for each site installation. This would allow a site manager to control what the defaults are for the search string, update rate and number of tweets displayed. To do that, I need to add a global configuration function to the block.

    In Moodle 1.9, global configuration was handled by the presence of a settings.php file in the block directory, and the function has_config defined to return "true". The good news, is that Moodle 2 does it exactly the same way, with some added differences I can take advantage of.

    To start with, I'll create my new settings.php file. Basically, I'm adding global versions of the local settings I have defined in edit_form.php. The settings developer documentation points me to information that will help with this, but I think I can fake it.

    Here is what I entered as my settings.php:
    <?php
    $settings->add(new admin_setting_configtext('block_twitter_search/defaultsearch',
                       get_string('defaultsearch', 'block_twitter_search'),
                       get_string('configdefaultsearch', 'block_twitter_search'), '#moodle', PARAM_TEXT));

    $settings->add(new admin_setting_configselect('block_twitter_search/defaultnumtweets',
                       get_string('defaultnumtweets', 'block_twitter_search'),
                       get_string('configdefaultnumtweets', 'block_twitter_search'), 10, range(0, 20)));

    $settings->add(new admin_setting_configtext('block_twitter_search/defaultpolltime',
                       get_string('defaultpolltime', 'block_twitter_search'),
                       get_string('configdefaultpolltime', 'block_twitter_search'), '30000', PARAM_INT));
    If you've created a global settings function for Moodle 1.9, you will see this isn't really different. But there is one thing I've done differently. For the name of the three configuration elements, I've added the prefix 'block_twitter_search/'. In the past, I would have named each element with a '_' instead of a '/'. What the '/' does is move the global configuration variable from the global namespace stored in the global $CFG object to a local namespace within the block object. Without the '/', I could access the variable directly from the $CFG global without any other code, but I would also be increasing the size of that global unnecessarily. If I use the block's namespace, the same variables will be available to me through the get_config API call. Using the '/', I am doing things the way Moodle prefers me to do.

    Note, that I also entered language strings for the settings, two per setting (one for the displayed setting label, and one for a description of what it's for).

    Now if I go to my 'Site admin / Plugins / Blocks' page, I can see "Twitter search" as one of the options in the menu. Clicking this link brings up the settings page I just created. I should also be able to access this page from the 'Manage blocks' main page by using the 'Settings' link next to my block. But, when I go there now, there is no 'Settings' link.

    There's a simple solution to getting this link. I need to add a has_config function to my class and return 'true'. I'm not sure of the reason, but Moodle uses the presence of the settings.php file to indicate that the global settings link should be present in the menu, and the has_config function to indicate that the settings link should be present on the 'Manage blocks' page. Having both ensures that the link will be available in both places.

    So now that I have a way to define the defaults, I need to add code so that they are used.

    The variables I defined for the global settings are available through the use of the function call, get_config($plugin, $name). I will add the use of this for each default to my specialization function code that currently has the defaults hard-coded. So, where before, the code looked like this:
    if(empty($this->config->search_string)) {
        $this->config->search_string = "#moodle";
    }
    Now, I make it look like this:
    if(empty($this->config->search_string)) {
        if (($defaultsearch = get_config('block_twitter_search', 'defaultsearch')) === false) {
            $defaultsearch = '#moodle';
        }
        $this->config->search_string = $defaultsearch; 
    }
    This will set the block setting to whatever default we specified in the global settings, if it exists. The use of ' === false' is necessary so that there is a  difference between a setting that is intentionally blank (or zero) and one that doesn't exist.

    For testing, I change my global settings to something other than the hardcoded values (I use, '#remote-learner', '5', and '60000'). Then I add a new instance of the block to a course and edit the settings for that block. Now I see my configured defaults instead. That's what I wanted.

    At this point, I'm going to call this block migration complete. I have zipped up my final code, and made it available through my public dropbox. Feel free to download and play with it. In my next series of posts, I'll tackle a Moodle activity plug-in.

    Thursday, November 3, 2011

    Converting M1.9 Plug-ins to M2 - Block Part 5 - Cleaning Up

    This is part six of my series concerning porting Moodle 1.9 code to Moodle 2.

    The next problem that I know is there, even though it doesn't seem to be causing any problems, is that the code is trying to include Javascript files that don't exist in the location specified.

    In the get_content function of the block, the file "/lib/yui/yahoo/yahoo-min.js" and "/lib/yui/dom/dom-min.js" are specified for inclusion in our HTML. These files are now at "/lib/yui/2.9.0/build/yahoo/yahoo-min.js" and "/lib/yui/2.9.0/build/dom-min.js". There actually is evidence of this problem on the page that is displayed; but its hidden in the HTML code. If I look at the code included in those statements, using something like the Firebug plug-in for Firefox, I can see a "404 Not Found" error embedded in the page.

    The easy fix would be to correct the URL's. But since this doesn't seem to be affecting the function, I think I should look and see if I can remove them completely. There are two documents I am looking at: "JavaScript guidelines" and "Migrating your code to the 2.0 rendering API". The second one I am using cautiously as much of it is obsolete.

    After reading through these, and then looking at existing code in Moodle 2, I believe I can replace these two HTML lines with a call to $PAGE->requires->yui2_lib(). This should add the necessary YUI code I need to my page if it is not already presently loaded. I think the line I need is:
    $PAGE->requires->yui2_lib('yahoo', 'dom');
    Unfortunately, making this change doesn't prove that I have fixed it, since there is no problem to verify. But I believe this is correct and will leave it as such. (If anyone can confirm this or knows what is correct, comment here and I'll fix this.)

    I also read that Javascript files should be included using the $PAGE->requires->js() function. This helps control when and where the Javascript file gets loaded, and helps to make sure that the same file isn't loaded more than once. An optional boolean argument, set to "true", will load the script in the header of the page. We will need this as we need the functions in there when the block is displayed. I change the line that includes the block's Javascript file in HTML to:
    $PAGE->requires->js('/blocks/twitter_search/javascript.js', true);
    Note: when I did this initially, I used the $CFG->wwwroot as in the older code as a prefix to the file name. This gave me an error which recoding as above fixed.
    The other Javascript I have in the block are the lines that set the callback function and the update link, for the refreshing of the tweets. There may be ways to do this differently, but I'm going to leave them for now. If anyone wishes to submit a better solution, I'll include it.

    I'm going to do one more thing. Currently, the block has defaults for the search string, update rate and the number of tweets to display. These are hardcoded into the block as "#moodle", "10" and "30000" (30 seconds). It would be nicer if the site could specify their own defaults without having to change the code. To do that, I am going to add a global configuration page. I'll leave that for the next post.

    Tuesday, November 1, 2011

    Converting M1.9 Plug-ins to M2 - Block Part 4 - Fixing Old Code

    This is part five of my series concerning porting Moodle 1.9 code to Moodle 2.

    I want to tackle the display bug I see when the block updates. If you remember, when I installed the working version of the block, it displayed the tweets initially, but displayed a "404" error for each update after that.

    My initial thoughts on this is that it is caused by multiple problems. Database API used in the "ajax_update.php" script, changed database table name and YUI scripts no longer located in the same places.

    To begin I open up the "ajax_update.php" script.

    This script is very small - it only contains the following lines of code:
    <?php
    require_once('../../config.php');
    $block = block_instance('twitter_search', get_record_select('block_instance','id="'.required_param("blockid",PARAM_NUMBER).'"'));
    echo $block->get_content()->text
    ?>
    It is small for a reason: it gets called repeatedly by the block through its Ajax javascript functions, to reload the tweets. It does this by reinstantiating the block instance and calling its get_content method.

    Looking at the code above, I see two problems: the table name for block instances has been changed from "block_instance" in Moodle 1.9, to "block_instances" in Moodle 2, and  it is using the old data API call, get_record_select. These both have to be fixed.

    The new database API is similar to the old one, but with significant changes. The ones I care about here are:
    • All database API functions are now methods of the global database object $DB.
    • All parameters to the database functions are now passed as array "key => value" pairs instead of individual parameters.
    I'm also going to break the old single line multiple function code line into three lines so its easier to see what is happening. When I'm done, the new code looks like this:
    require_once '../../config.php';
    $id = required_param('blockid', PARAM_NUMBER);
    $bi = $DB->get_record('block_instances', array('id' => $id));
    $block = block_instance('twitter_search', $bi);
    echo $block->get_content()->text;
    The main changes are in the third line. Here I have changed get_record_select to $DB->get_record, changed the table name to the new name and passed the "id" argument in a hashed array format. (Note that I don't have to declare $DB as a global, as this script is running at the top level and not in a function call.) I'm going to save this code and see if it fixes my problem.

    I reload my page with the block, click the "update" link, and I see new tweets. No more "404" error. But, there are a bunch of warnings now, indicating that I have a coding error involving $PAGE->context. Specifically:
    Coding problem: $PAGE->context was not set. You may have forgotten to call require_login() or $PAGE->set_context(). The page may not display correctly as a result
    This is kind of a strange one, as the problem is that Moodle 2 is telling us that the page may not display correctly because I haven't called all the necessary functions that tells the $PAGE object what I am outputting. Of course, since it is outputting into an already existing block on a page using Ajax, this isn't really a problem. Still, there must be something I can do to fix this.

    It looks like all I need to do is call "require_login" and "$PAGE->set_context" before doing any output. I'll add require_login right after the require_once statement. Since the $PAGE->set_context requires an actual context to be passed to it, I'll add it right after I create the $block object and pass it the block's context. So the statement is $PAGE->set_context($block->context).

    Now I'll look at the page again. Success! The warnings are gone, and I'm getting tweet updates.

    For the record, my code now looks like:
    require_once '../../config.php';
    require_login();
    $id = required_param('blockid', PARAM_NUMBER);
    $bi = $DB->get_record('block_instances', array('id' => $id));
    $block = block_instance('twitter_search', $bi);
    $PAGE->set_context($block->context);
    echo $block->get_content()->text;
    I still haven't tackled the YUI library location changes, but the block is working, so I'll look at those in the next post.