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.