Monday, December 19, 2011

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


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

When I left off, I had completed the necessary changes the allowed me to successfully install, add and edit instances of the stamp collection module. Now it is time to start making it function.

In Moodle 2, the entire navigation and page display functions have changed. I need to change all of the code that does this. This document describes the changes, but it is outdated so there will be inconsistencies.

To check where my code is, I click on an instance of a module that I've added to a course. Right away I see several warnings and errors I'll need to fix. I'll start with my "view.php" script.

One of the first things I need is to set the URL of my display page into the global $PAGE variable. This identifies the page's URL and any necessary parameters to the internal systems, such as navigation, so that links back to the page get rendered properly. In my module, I add the lines:
$params = array();
$params['id'] = $id;
if ($view) {
    $params['view'] = $view;
}
if ($page) {
    $params['page'] = $page;
}
$PAGE->set_url('/mod/stampcoll/view.php', $params);
before the require_course_login function.

This code, combined with the require_course_login call, pretty much sets up everything I need for the page and navigation display, and gets rid of the first group of warnings..

Next on my list is changing all of the output functions to use the global $OUTPUT variable. Essentially, everything that used to write output to the screen should now come from a function of the object $OUTPUT. So print_box would become $OUTPUT->box, and so on.

I'll start with the print_header functions. I find two, both very similar to this:
$navigation = build_navigation('', $cm);
print_header_simple(format_string($stampcoll->name), "",
                 $navigation, "", "", true, '', navmenu($course, $cm));
In Moodle 2, these functions are replaced with the simpler $OUTPUT->header(), but I need to first specify all of the arguments that were passed to the old function. Also, the navigation has already been handled by the page setup functions and the course login functions. So I completely remove these lines of code. Then, before the code block where the first print_header_simple was, I add the lines:
$PAGE->set_title(format_string($stampcoll->name));
$PAGE->set_heading(format_string($course->fullname));
echo $OUTPUT->header();
These three lines replace the navigation and header printing functions for the script, and when I access the module instance, displays the page heading again.

Next up, I look for the "print_" functions. When I look at a page of my module now, I can see warnings for these functions. The ones I replace are the "print_heading" and the "print_box..." functions. I believe I can pretty much simply replace "print_" with "echo $OUTPUT->". After doing that, and revisiting the screen, the warnings are gone.

Next, I click on the "Edit stamps" tab link. I get essentially the same warning and errors as on the view page. Looks like I need to make the same changes in "editstamps.php" as I did in "view.php".

Now, I'll do some testing as a student. I login as a student and access the activity. Since I have no stamps, I am presented with an empty display and the message "Number of your stamps: $a". I'm pretty sure that "$a" should not be there. This will require a change to the language files, "lang/*/stampcoll.php".

Essentially, in Moodle 1.9 and below, language strings could specify variables that would be loaded into the string at runtime with the string "$a". In Moodle 2, the "$a" needs to be enclosed in braces ("{}"). I open the "lang/en/stampcoll.php" file (the language I'm using), and replace all "$a" with "{$a}" and save. Before I check if it worked, I remember to go to the "Site administration / Development" menu and click "Purge all caches". Going back to my stamp collection instance as a student, and clicking it, shows that the message now says: "Number of your stamps: 0". That seems more correct.

Okay, now that I have students in my course, I should go back and check how the adding stamps functions works. Accessing it again as the "admin" user, I get a problem right away:
ERROR: Mixed types of sql query parameters!!

More information about this error
Stack trace:

    line 704 of /lib/dml/moodle_database.php: dml_exception thrown
    line 804 of /lib/dml/mysqli_native_moodle_database.php: call to moodle_database->fix_sql_params()
    line 191 of /mod/stampcoll/view.php: call to mysqli_native_moodle_database->get_records_sql()
This is a problem that I had not encountered yet. Doing some debugging, I discover that the code:
if ($where = $table->get_sql_where()) {
    $where .= ' AND ';
}
no longer works as it did before. In fact, it now works in a similar fashion to the $DB->get_in_or_equal() function. I need to do some heavier refactoring in this code.


First, I need to change the code above to:
list($where, $w_params) = $table->get_sql_where();
if ($where) {
    $where .= ' AND ';
}
I also discover another problem. I am using a named parameter in my SQL, ":stampcollid". But the get_sql_where and the get_in_or_equal functions use positional parameters for the SQL. This is the "mixed types of sql query parameters" part of the error message. What this means is that the parameter array I pass to my SQL query must provide the replacement arguments in the same order they appear in the SQL query. My SQL query must use "?" instead of ":variable" replacement strings. So refactoring, this chunk of code now looks like:
list($where, $w_params) = $table->get_sql_where();
if ($where) {
    $where .= ' AND ';
}

if ($sort = $table->get_sql_sort()) {
    $sort = ' ORDER BY '.$sort;
}

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

$params = array();
$params[] = $stampcoll->id;
$params = array_merge($params, $w_params);
$params = array_merge($params, $u_params);

$sql = "FROM {user} u ".
       "LEFT JOIN {stampcoll_stamps} s ON u.id = s.userid AND s.stampcollid = ? ".
       "WHERE $where (u.id $uids) ".
       "GROUP BY u.id, u.firstname, u.lastname, u.picture ";

if (!$stampcoll->displayzero) {
    $sql .= 'HAVING COUNT(s.id) > 0 ';
}
Note that I have replaced ":stampcollid" with "?", and moved around the order that the $params array gets loaded so that the $where and $uids positions appear after the first "?".

One more issue crops up after this. The $OUTPUT->user_picture is not passing the correct parameters. This is because I simply replaced the old print function with $OUTPUT-> one. The old function took a user id. The new one takes a user object. The other parameters are different too, so I change it to:
$picture = $OUTPUT->user_picture($auser, array('courseid' => $course->id));
This gets me most of the way there. But I'm still getting an error about a missing "imagealt" field. A little research, and I determine that the user records that I am passing to the user_picture function are missing a field that is required by that function. Looking at other examples, I notice that the are all calling a new function, user_picture::fields, to get the fields they request from their database call for user records. It appears that this function returns a list of user record field names suitable for providing in an SQL query, that are the minimum required for other user functions. This seems to be the new preferred way of specifying user record fields, rather than specifying them directly in the SQL. So to fix my code, I replace:

$select = 'SELECT u.id, u.firstname, u.lastname, u.picture, COUNT(s.id) AS count ';
with:
$userfields = user_picture::fields('u');
$select = "SELECT {$userfields}, COUNT(s.id) AS count ";
This change makes the "imagealt" error go away, and displays the user pictures in the list.

Next, I am getting errors related to using helpbutton(). Essentially, the use of this function has been replaced by $OUTPUT->help_icon. This is similar to what was changed in the forms. Basically, the old way of providing a full help file that appeared in a pop-up, has been replaced by a help string that shows in a mouse-over tip. So, I have to replace the function call, and move the help message from the help file into the language file as a language string.

The first change is changing:
helpbutton('pagesize', get_string('studentsperpage','stampcoll'), 'stampcoll');
to:
echo $OUTPUT->help_icon('studentsperpage', 'stampcoll');
In the old version, a help icon labeled with the language string "studentsperpage" would be rendered, that when clicked, would load a pop-up with the file "pagesize.html" in it. In the new version, an icon will be displayed that when clicked, will display the string from "studentsperpage_help" in the pop-up tip window. So, I also need to bring the text from the old help file into a new language string. In this case, there was no help file in the old version, so I'll just create a new string:
$string['studentsperpage_help'] = 'Set the number of students you want to display per page';
I have to make this same series of changes in "editstamps.php" and one more for a "showupdateforms" help string.

At this point, I have migrated all of the basic functions in the main files to Moodle 2. I still have work to do though. I need to check the rest of the scripts for all of the same changes I have performed so far, and there are still more migration tasks. For now, I am calling it a day.

Friday, December 9, 2011

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

When I left off, I had completed bulk changing the DML function code for the module. Now I'll move on to other functional and output changes.

One of the key changes that affects this module is the standard use of the "intro" field. This is the field that typically provided the textual description for an instance of the module. In Moodle 1.9, some modules called it "intro". Others called it "description". This module called it "text". The point is, it was not standard and was not necessarily the same in all modules. In Moodle 2, it is standard, and needs to be called "intro".

To do this, I will first need to change the database code that sets it up, and add an upgrade script for existing installations.

For the "install.xml" file, there are three changes of "text" to "intro" I need to do: the actual entry for the "text" field's "NAME" parameter and the two entries on either side of it's "NEXT" and "PREVIOUS" parameters.

Along with "intro", Moodle 2 now requires that the module have an "introformat" field in the database table. This needs to be added to the "install.xml" file as well as the upgrade script. For this module, I need to change the old "format" field to "introformat". I'm not including the XML code here, but you can see it if you check it out of Git.

Next, I need to add an upgrade script to handle changing the database field name for sites that are upgrading from an older version. I add the following lines to the bottom of "db/upgrade.php" in my module:
if ($oldversion < 2010080300) {

/// Rename field text on table stampcoll to intro
    $table = new xmldb_table('stampcoll');
    $field = new xmldb_field('text', XMLDB_TYPE_TEXT, 'small', null, XMLDB_NOTNULL, null, null, 'name');

/// Launch rename field description
    $dbman->rename_field($table, $field, 'intro');


/// Define field introformat to be added to data
    $field = new xmldb_field('format', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'intro');

/// Launch rename field introformat
    $dbman->rename_field($table, $field, 'introformat');

    // conditionally migrate to html format in intro
    if ($CFG->texteditors !== 'textarea') {
        $rs = $DB->get_recordset('stampcoll', array('introformat'=>FORMAT_MOODLE), '', 'id,intro,introformat');
        foreach ($rs as $d) {
            $d->intro       = text_to_html($d->intro, false, false, true);
            $d->introformat = FORMAT_HTML;
            $DB->update_record('stampcoll', $d);
            upgrade_set_timeout();
        }
        $rs->close();
    }
/// stampcoll savepoint reached
    upgrade_mod_savepoint(true, 2010080300, 'stampcoll');
}
This block of code will rename the "text" field of the "stampcoll" table to "intro" and the "format" field to "introformat". It will also reset the format values where necessary for existing instances. Just to be sure, I test this by manually changing the version of the "stampcoll" entry to a lesser number, and then hitting the "Notifications" screen of my site. As expected, the "stampcoll" table "text" and the "format" fields are renamed.

Lastly I test the XML changes by removing the "stampcoll" module, and then allowing it to be reinstalled from the "Notifications" screen. This works too.

After making those changes, I need to then go and change every use of the "text" variable to "intro". This will also be important in my restore code that I will deal with later on, since older restore sets will be using "text" and not "intro". Searching, and being careful to find only the correct places, I replace the few "text" instances that need replacing (in "lib.php" and "view.php"). There will be changes in backup and restore as well, but I'm doing that later.

I don't need to worry about "introformat", since that is a variable used internally by the API, and not directly by my code. 

Next, I'm going to change the module edit form in "mod_form.php". Most of this remains the same as before, but there are some crucial differences.

The first and easiest is changing the "intro" component. In Moodle 1.9. this involved several lines. In Moodle 2, thanks to the standardization of the "intro" field (which I just fixed), it is now only one line. So I change the lines:
/// text (description)
    $mform->addElement('htmleditor', 'text', get_string('description'));
    $mform->setType('text', PARAM_RAW);
    //$mform->addRule('text', get_string('required'), 'required', null, 'client');
    $mform->setHelpButton('text', array('writing', 'richtext'), false, 'editorhelpbutton');
/// introformat
    $mform->addElement('format', 'introformat', get_string('format'));
to:
/// intro
    $this->add_intro_editor(true, get_string('description'));
The next change I have to make involves the help system. Now, in this particular module, there are no specific help buttons added. But if there were, I would need to change each occurrence of:
$mform->setHelpButton('elementname', array('helpfilename', get_string('helpstring', 'component'), 'component'));
to:
$mform->addHelpButton('elementname', 'helpstring', 'component');
The function name changes, and the arguments are simplified. Additionally, all old "help" files in your language directories need to removed, and their contents added to the main language file, assigned to the string "helpstring_help".

One thing to consider at this point, is that the help buttons that are added now are mouseover text areas. The amount of text displayed there should be suitably limited. If you need more help content, you can create HTML files elsewhere, and link to them in the help string. Moodle docs provides a space to create this content if you want to use it.

One thing I will change for this module is the help text for the main edit screen, up next to the title. In Moodle 1.9, this content was in the "lang/en_utf8/help/stampcoll/mods.html" file. In Moodle 2, this is in the language string "modulename_help". I add a shortened version of this text to that string in my "lang/en/stampcoll.php" file.

These changes should be all I need to be able to add and edit instances of the module. I test it to make sure, and indeed I can successfully add and edit instances. I'm far from done though. Clicking on an added module will display a lot of errors still. In the next part, I'll work on fixing those.

Make sure you get the latest code from Git, to follow along.