Monday, November 5, 2012

Adding Moodle 1.9 Block Restore to Moodle 2.3 - Part 5

In my last post, I had determined that I could make things happen correctly through the correct use of the get_paths() function creating convert_path objects. This led me to believe that I could now plan out the changes necessary to complete this task.

If you have followed along with the MDL-32880 tracker issue, you will notice that there has been some activity from other participants. Particularly, Paul Nicholls has done some work in his repository and proposed some changes. While I won't use all of them, some of them will make my work easier.

I know that the moodle1_handlers_factory::get_handlers() function (in file "/backup/converter/moodle1/handlerlib.php") uses the get_plugin_handlers() function to load all handlers it finds for blocks. But, since it expects to find specific class files and functions for any block to convert, it doesn't do anything for blocks (since none of the blocks provide this function). Paul's solution involves adding the appropriate file and function to each block in core to make the conversions happen. I still believe I can do this for every existing block without having to create new class files for every block.

I have looked closely at the functions in "handlerlib.php", and my plan is to do almost all of the work in there. My plan is:
  • Use the existing moodle1_block_handler class as the main handler. It will define all of the default, necessary processes for any block conversion.
  • Extend that class as moodle1_block_generic_handler, for use by blocks that don't require their own conversion handlers. This extended class will simply call all of the default functions.
  • Any block that requires more than the default conversions can extend and override this class with their own derived class, using the convention moodle1_block_[blockname]_handler.
  • Modify the get_plugin_handlers function to load the generic or block specific handler depending on the block. If a specific block handler class exists it will load it, otherwise it will load the generic class.
For my plan, the best place to start is with the main handler, defining the default processes. This would be adding a get_paths and a process_block function to the moodle1_block_handler. At this point, I want to take a look at what Paul Nicholls has proposed.

In Paul's changes, he has provided a moodle1_block_handler::process_block() function that pretty much does everything we need for all of the blocks. He has also provided a specific get_paths() function in every block to define the use of the process_block function. I can use the first part, but I will create a generic get_paths() function instead.

To use Paul's process_block function, I could just cut and paste his code into my version of the file. But instead, I am going to use the strength of Open Source collaboration and pull his changes into mine using Git.

I already have a local version of my Git repository checkout from my "github" repository, with a working branch for this issue. My local repository also has the main Moodle repository added as a remote named "upstream" to allow me to get the latest Moodle code from HQ. I can add Paul's as a new "remote" to my local one allowing me to "pull" commits from his. From the tracker issue, Paul's repository is https://github.com/pauln/moodle/ and he has documented specific commits for his work. So, in my local Git repository I issue the commands:
git fetch upstream
git pull upstream MOODLE_23_STABLE
This makes sure I have all of the latest Moodle 2.3 code in my local repository before I do any more changes. Next, I issue:
git remote add pauln git://github.com/pauln/moodle.git
git fetch pauln
This attaches Paul's repository to mine as a remote, and gets his latest update information. Now, the next command is the important one. I can grab just the changes he made to moodle1_block_handler by cherry picking the commit that modified that code, using the specific commit hash associated with it. I issue:
git cherry-pick 76a7c44491120a696104cd5a4755890c1968777b
This pulls in Paul's changes that added the process_block function to the moodle1_block_handler and maintains his commit information so that his changes are credited to him. This allows me to use his work without having to duplicate it. Once complete, I have the function:
public function process_block(array $data) {
    $newdata = array();
    $instanceid     = $data['id'];
    $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);

    $newdata['blockname'] = $data['name'];
    $newdata['parentcontextid'] = $this->converter->get_contextid(CONTEXT_COURSE, 0);
    $newdata['showinsubcontexts'] = 0;
    $newdata['pagetypepattern'] = $data['pagetype'].='-*';
    $newdata['subpagepattern'] = '$@NULL@$';
    $newdata['defaultregion'] = ($data['position']=='l')?'side-pre':'side-post';
    $newdata['defaultweight'] = $data['weight'];
    $newdata['configdata'] = $data['configdata'];

    // block.xml
    $this->open_xml_writer("course/blocks/{$data['name']}/block.xml");
    $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid));

    foreach ($newdata as $field => $value) {
        $this->xmlwriter->full_tag($field, $value);
    }

    $this->xmlwriter->begin_tag('block_positions');
    $this->xmlwriter->begin_tag('block_position', array('id' => 1));
    $this->xmlwriter->full_tag('contextid', $newdata['parentcontextid']);
    $this->xmlwriter->full_tag('pagetype', $data['pagetype']);
    $this->xmlwriter->full_tag('subpage', '');
    $this->xmlwriter->full_tag('visible', $data['visible']);
    $this->xmlwriter->full_tag('region', $newdata['defaultregion']);
    $this->xmlwriter->full_tag('weight', $newdata['defaultweight']);
    $this->xmlwriter->end_tag('block_position');
    $this->xmlwriter->end_tag('block_positions');
    $this->xmlwriter->end_tag('block');
    $this->close_xml_writer();

    // inforef.xml
    $this->open_xml_writer("course/blocks/{$data['name']}/inforef.xml");
    $this->xmlwriter->begin_tag('inforef');
    // TODO: inforef contents if needed
    $this->xmlwriter->end_tag('inforef');
    $this->close_xml_writer();
     // roles.xml
    $this->open_xml_writer("course/blocks/{$data['name']}/roles.xml");
    $this->xmlwriter->begin_tag('roles');
    $this->xmlwriter->begin_tag('role_overrides');
    // TODO: role overrides if needed
    $this->xmlwriter->end_tag('role_overrides');
    $this->xmlwriter->begin_tag('role_assignments');
    // TODO: role assignments if needed
    $this->xmlwriter->end_tag('role_assignments');
    $this->xmlwriter->end_tag('roles');
    $this->close_xml_writer();

    return $data;
}
The other piece I need is a generic get_paths function, that creates convert_path objects for each block without having to add it to the specific block's code base. Since moodle1_block_handler is an extension of moodle1_plugin_handler, the name of the plugin (the block in this case) is stored in an object variable called pluginname. I can use this to create a default, block-aware get_paths function:

public function get_paths() {
    $blockname = strtoupper($this->pluginname);
    return array(
        new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"),
    );
}
This pretty much gives me everything I should need to convert default blocks to Moodle 2 restore data. Now, since moodle1_block_handler is an abstract class, I need to extend a new class that uses it so that it can be executed. This will be my moodle1_block_generic_handler class. I create:

/**
 * Base class for block generic handler
 */
class moodle1_block_generic_handler extends moodle1_block_handler {

}
At this point, that's all I need in  that class, since it will just be using the default functions.

The last thing I need to do is to modify the get_plugin_handlers function to load the proper handling class for each block it finds. Currently, it only loads a class if a specific handler exists for each block. I want to change it so that it loads a specific handler if it exists, otherwise it loads the generic class. I modify the existing get_plugin_handlers class as follows:
protected static function get_plugin_handlers($type, moodle1_converter $converter) {
    global $CFG;

    $handlers = array();
    $plugins = get_plugin_list($type);
    foreach ($plugins as $name => $dir) {
        $handlerfile  = $dir . '/backup/moodle1/lib.php';
        $handlerclass = "moodle1_{$type}_{$name}_handler";
        if ($type != "block") {
            if (!file_exists($handlerfile)) {
                continue;
            }
            require_once($handlerfile);
        } else {
            if (!file_exists($handlerfile)) {
                $handlerclass = "moodle1_block_generic_handler";
            }
        }

        if (!class_exists($handlerclass)) {
            throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
        }
        $handlers[] = new $handlerclass($converter, $type, $name);
    }
    return $handlers;
}
Essentially, if the plugin type is not a block, continue doing what it always did. If it is a block, and there is no specific block handler defined, then load the generic block handler moodle1_block_generic_handler.

If you want to see the specific code I used, you can see it by the specific commits:
https://github.com/mchurchward/moodle/commit/5036ed80ad57c65c8cc18409e0b59307e0061c52
and
https://github.com/mchurchward/moodle/commit/b346f7e63c9f105dc27fc47707349b90e172ad52

With these changes, I rerun a test restore of a 1.9 backup file with blocks, and it works! The restored course contains the blocks that were in the 1.9 site. But I'm not done yet. I have to handle the blocks that have extra restore needs. That will be in the next post.