<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Jocelyn Ireson-Paine&apos;s Blog</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/" />
    <link rel="self" type="application/atom+xml" href="http://www.j-paine.org/blog/atom.xml" />
    <id>tag:www.j-paine.org,2010-04-29:/blog//3</id>
    <updated>2012-01-13T08:52:39Z</updated>
    
    <generator uri="http://www.sixapart.com/movabletype/">Movable Type 5.01</generator>

<entry>
    <title>My Own Web-service Function: one that doubles its arguments</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2012/01/my-own-web-service-function-one-that-doubles-its-arguments.html" />
    <id>tag:www.j-paine.org,2012:/blog//3.246</id>

    <published>2012-01-13T08:47:57Z</published>
    <updated>2012-01-13T08:52:39Z</updated>

    <summary>Having got the &quot;hello World&quot; &apos;wstemplate&apos; Web-service plugin working as described in last night&apos;s post, I tried modifying it to create a function of my own, that doubles its argument. This is how. I&apos;d unzipped the plugin into a directory...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
        <category term="Moodle" scheme="http://www.sixapart.com/ns/types#category" />
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<p>Having got the "hello World" 'wstemplate' Web-service plugin working as described <a href="http://www.j-paine.org/blog/2012/01/at-general-plugins-local-web.html">in last night's post</a>, I tried modifying it to create a function of my own, that doubles its argument. This is how. I'd unzipped the plugin into a directory called wstemplate. First, I edited wstemplate/externallib.php:</p>
<pre>require_once($CFG-&gt;libdir . "/externallib.php");

class local_jnip_external extends external_api 
{
  /**
   * Returns description of method parameters.
   * @return external_function_parameters
   */
  public static function double_parameters() 
  {
    return new external_function_parameters(
      array( 'n' =&gt; new external_value( PARAM_TEXT
                                      , 'The number to be doubled.'
                                      )
           )
    );
  }


  /**
   * Returns welcome message.
   * @return string welcome message
   */
  public static function double( $n ) 
  {
    global $USER;

    // Parameter validation.
    // REQUIRED.
    $params = self::validate_parameters( self::double_parameters()
                                       , array( 'n' =&gt; $n ) 
                                       );

    // Context validation.
    // OPTIONAL. Not needed for this function.

    // Capability checking.
    // OPTIONAL. Not needed for this function.

    return $params['n'] * 2;
  }


  /**
   * Returns description of method result value.
   * @return external_description
   */
  public static function double_returns() 
  {
    return new external_value( PARAM_TEXT, 'The argument doubled.' );
  }
}
</pre>
<p>In the above, I've changed the classname to local_jnip_external. (My initials are JNIP). I've replaced 'hello_world' by 'double' in the names of the three functions above. I've also altered the parameter and result descriptions so that they described double's argument and result. Unlike hello_world, double doesn't have a default value for its parameter, so I have removed that part of the parameter description. The parameter name, I've changed to 'n'.</p>
<p>And I've removed the capability check at the end of 'double'. I don't know how likely one would be to write some kind of arithmetic Web service in real life -- perhaps one would for a statistics add-on -- but I can't see that any security checks are needed at all. Therefore, I have also removed the context validation.</p>
<p>Next, I edited wstemplate/db/services.php:</p>
<pre>// We define the Web-service functions to install.
//
$functions = array(
        'local_jnip_double' =&gt; array(
                'classname'   =&gt; 'local_jnip_external',
                'methodname'  =&gt; 'double',
                'classpath'   =&gt; 'local/jnip/externallib.php',
                'description' =&gt; 'Doubles its argument.',
                'type'        =&gt; 'read',
        )
);

// We define the services to install as pre-built services. 
// A pre-built service is not editable by the administrator.
//
$services = array(
        "Jocelyn's service" =&gt; array(
                'functions' =&gt; array ('local_jnip_double'),
                'restrictedusers' =&gt; 0,
                'enabled'=&gt;1,
        )
);
</pre>
<p>Here, I've changed the outer key in the $functions array to local_jnip_double, and the class name, method name, class path, and description in the inner array to their new values. In the $services array, I've changed the service name and function name.</p>
<p>I left wstemplate/version.php and wstemplate/lang/en/local_wstemplate.php alone. Then I changed the directory name from wstemplate to jnip, so that on my Moodle's Linux host, it sat at /var/www/moodle/moodle/local/jnip/ .</p>
<p>To make Moodle install the plugin, I went to Site administration &gt; Notifications. Moodle had noticed the new directory, and showed it to me in a plugins table. I pressed the Upgrade button on that page, and then Continue on the next page.</p>
<p>I then went to Site administration &gt; Plugins &gt; Web services &gt; External services, and saw a table listing my services, including a row for "Jocelyn's service". I clicked on the Functions link -- and got an error saying that the description of the return value was corrupt. I'd mistyped it. So I corrected it in externallib.php . (The code above shows the corrected one.) Then I had to make Moodle notice this update. I remembered reading that it notices when the version number in a version.php file has been increased. So I edited wstemplate/version.php. The original version, the one I'd copied from wstemplate, looked like this:</p>
<pre>$plugin-&gt;version  = 2011101202;   // The (date) version of this module + 2 extra digital for daily versions
                                  // This version number is displayed into /admin/forms.php
                                  // TODO: if ever this plugin get branched, the old branch number
                                  // will not be updated to the current date but just incremented. We will
                                  // need then a $plugin-&gt;release human friendly date. For the moment, we use
                                  // display this version number with userdate (dev friendly)
$plugin-&gt;requires = 2010112400;  // Requires this Moodle version - at least 2.0
$plugin-&gt;cron     = 0;
$plugin-&gt;release = '1.0 (Build: 2011101202)';
$plugin-&gt;maturity = MATURITY_STABLE;
</pre>
<p>I wasn't really sure what to do -- I've never seen a Moodle document about the correct care and feeding of version files -- but it seemed reasonable to replace both occurrences of 2011101202 by 2011101203. I then went back to Site administration &gt; Notifications. Again, Moodle showed me the table of plugins, saying that this one needed upgrading. So it had noticed the change. I pressed Upgrade, and then Continue.</p>
<p>For my client, I wrote this program:</p>
<pre>  echo "Demo of Web service doubling function using REST\n";
  echo "================================================\n";

  $token = '401eae223b346c10c3298d74itv7ddf5';
  $domain = 'http://moodle.ireson-paine.com';

  $n = 5;

  $function_name = 'local_jnip_double';

  $params = array( 'n' =&gt; $n );

  $serverurl = $domain . '/webservice/rest/server.php'. '?wstoken=' . $token . '&amp;wsfunction='.$function_name;

  require_once( './curl.php' );
  $curl = new curl;

  echo "\nAbout to call function.\n";

  $response = $curl-&gt;post( $serverurl, $params );

  echo "\nCalled function.\n";

  echo "\nResponse = $response.\n";
</pre>
<p>The token in it, I made by going to Site administration &gt; Plugins &gt; Web services &gt; Manage tokens, and adding a new token for my Web-services user and the service called "Jocelyn's service".</p>
<p>And running the client produced this output:</p>
<pre>Demo of Web service doubling function using REST
================================================

About to call function.

Called function.

Response = &lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;RESPONSE&gt;
&lt;VALUE&gt;10&lt;/VALUE&gt;
&lt;/RESPONSE&gt;
.
</pre>
<p>I also tried an XML-RPC version of the client, like this:</p>
<pre>  echo "Demo of Web service doubling function using XML-RPC\n";
  echo "===================================================\n";

  $token = '401eae223b346c10c3298d74itv7ddf5';
  $domain = 'http://moodle.ireson-paine.com';

  $n = 5;

  $function_name = 'local_jnip_double';

  $params = array( $n );

  $serverurl = $domain . '/webservice/xmlrpc/server.php'. '?wstoken=' . $token . '&amp;wsfunction='.$function_name;

  require_once( './curl.php' );
  $curl = new curl;

  echo "\nAbout to call function.\n";

  $post = xmlrpc_encode_request( $function_name, $params );

  $response = xmlrpc_decode($curl-&gt;post($serverurl, $post));

  echo "\nCalled function.\n";

  echo "\nResponse = "; print_r($response); echo ".\n";
</pre>
<p>But I got an error message saying that the parameter didn't match its description. This happened also when I set $params to array( n=&gt;$n ). I haven't worked out what I should write, but because I'm likely to keep on using REST rather than XML-RPC, I'm not worrying about it.</p>]]>
        
    </content>
</entry>

<entry>
    <title>Making the &quot;Hello World&quot; Web-service function work</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2012/01/at-general-plugins-local-web.html" />
    <id>tag:www.j-paine.org,2012:/blog//3.247</id>

    <published>2012-01-13T03:00:00Z</published>
    <updated>2012-01-13T08:51:13Z</updated>

    <summary>At General plugins (Local): Web service template, there&apos;s a local plugin that installs a new Web-service function. It&apos;s a very simple function that returns the message &quot;Hello, Web&quot;. I&apos;ve been trying this, and got it to work, but only after...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
        <category term="Moodle" scheme="http://www.sixapart.com/ns/types#category" />
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<p>At <a href="http://moodle.org/plugins/pluginversion.php?id=401"><em>General plugins (Local): Web service template</em></a>, there's a local plugin that installs a new Web-service function. It's a very simple function that returns the message "Hello, Web". I've been trying this, and got it to work, but only after missing out one important step. So I thought it might be useful if I were to describe how I installed it: a checklist that others can follow if they have problems. The client that calls the function uses the XML-RPC protocol, but I'll also show a version that uses REST.</p>

<p>These are the steps I took:</p>

<ul><li>

<p>I downloaded the plugin from <a href="http://moodle.org/plugins/pluginversion.php?id=401">http://moodle.org/plugins/pluginversion.php?id=401</a> by clicking on the orange "Download for Moodle 2.0/Moodle 2.1/Moodle 2.2" box.</p>

</li>

<li>

<p>The download arrived as a zip file. I copied it into my Moodle's local directory. Incidentally, this is /var/www/moodle/moodle/local .</p>

</li>

<li>

<p>I unzipped the zip file there. This created a subdirectory named 'wstemplate' containing client, db, externallib.php, lang, README, and version.php .</p>

</li>

<li>

<p>From there, I downloaded local/wstemplate/client/client.php and local/wstemplate/client/curl.php onto my PC.</p>

</li>

<li>

<p>On my Moodle, I went to Site administration &gt; Notifications. This gave me a table of three plugins, the final one of which had this row:</p>

<pre>Web service template   /local/wstemplate   Extension   2011101202    Moodle 2010112400   To be installed .
</pre>

<p>I pressed the 'Upgrade' button below the table. This brought up a page saying</p>

<pre> 
local_wstemplate
Success
</pre>

<p>I pressed the 'Continue' button on it. This brought up a blank Notifications page.</p>

</li>

<li>

<p>I went to Site administration &gt; Advanced features, and checked that the "Enable web services" checkbox was ticked.</p>

</li>

<li>

<p>I went to Site administration &gt; Plugins &gt; Web services &gt; Manage protocols, and checked that the checkbox for XML-RPC protocol was ticked.</p>

</li>

<li>

<p>I went to Site administration &gt; Plugins &gt; Web services &gt; External services. This showed a table with the line</p>

<pre>My service   local_wstemplate   Functions   All users   Edit .
</pre>

<p>So the service had evidently been installed. Clicking on "Functions" showed another table saying</p>

<pre>local_wstemplate_hello_world   Return Hello World FIRSTNAME. 
                                    Can change the text (Hello World) 
                                    sending a new text as parameter
</pre>
<p>That concluded the installation instructions at the top of client.php .</p>

</li>

<li>

<p>I went to Site administration &gt; Plugins &gt; Web services &gt; Manage tokens, and clicked the "Add" link. (If you've already created any tokens, this appears below a table of them.) For the user, I selected the Web-service user that I'd created according to the instructions in Site administration &gt; Plugins &gt; Web services &gt; Overview when I began experimenting with Web services last week. For the service, I selected 'My Service'.</p>

<p>By the way, I'm not sure whether I needed to use my Web-services user, which I'd given special capabilities, or whether any would do. The plugin contains a source file services.php in the db subdirectory, in which there's an assignment to the $services array. (The conventions are explained at the bottom of <a href="http://docs.moodle.org/dev/Creating_a_web_service_and_a_web_service_function"><em>Creating a web service and a web service function</em></a>.) This sets up the service so that it is not restricted to a particular user. So I may have been able to select any user when creating my token.</p>

</li>

<li>

<p>On my PC, I edited client.php to make $token the token just created. And also changed $domainname to be my Moodle's URL.</p>

</li>

<li>

<p>And I ran client.php by giving the DOS command php client.php . This displayed the message "Hello, Web".</p>

</li>

</ul><p>Having got that working, I modified the client so that it used the REST protocol. (That will work with the instructions above, if in the 'Manage protocols' step, you also enable REST.) This is the source code:</p>

<pre>  echo "Demo of Web service Hello World using REST\n";
  echo "==========================================\n";

  $token = 'bb4314dsp0mdd4f986fd4fb3ac2itv59';
  $domain</a> = 'http://moodle.ireson-paine.com';

  $welcomemsg = 'Hello, ';

  $function_name = 'local_wstemplate_hello_world';

  $params = array( 'welcomemessage' =&gt; $welcomemsg );

  $serverurl = $domain . '/webservice/rest/server.php'. '?wstoken=' . $token . '&amp;wsfunction='.$function_name;

  require_once( './curl.php' );
  $curl = new curl;

  echo "\nAbout to call function.\n";

  $response = $curl-&gt;post( $serverurl, $params );

  echo "\nCalled function.\n";

  echo "\nResponse = $response.\n";
</pre>

<p>The coding style is slightly different than for client.php, because I use different variable names and messages. For comparison, this is client.php rewritten in the same style:</p>

<pre>  echo "Demo of Web service Hello World using XML-RPC\n";
  echo "=============================================\n";

  $token = 'bb4314dsp0mdd4f986fd4fb3ac2itv59';
  $domain = 'http://moodle.ireson-paine.com';

  $welcomemsg = 'Hello, ';

  $function_name = 'local_wstemplate_hello_world';

  $params = array( $welcomemsg );

  $serverurl = $domain . '/webservice/xmlrpc/server.php'. '?wstoken=' . $token . '&amp;wsfunction='.$function_name;

  require_once( './curl.php' );
  $curl = new curl;

  echo "\nAbout to call function.\n";

  $post = xmlrpc_encode_request( $function_name, $params );

  $response = xmlrpc_decode($curl-&gt;post($serverurl, $post));

  echo "\nCalled function.\n";

  echo "\nResponse = "; print_r($response); echo ".\n";
</pre>
</p>]]>
        
    </content>
</entry>

<entry>
    <title>Copying Courses from Backups</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2012/01/copying-courses-from-backups.html" />
    <id>tag:www.j-paine.org,2012:/blog//3.239</id>

    <published>2012-01-10T09:30:13Z</published>
    <updated>2012-01-10T09:44:06Z</updated>

    <summary>In our Moodle, we will have a collection of courses written by experts and saved as course backups. We want new Moodle users (teachers or managers) to be able to copy these, then add their own teachers and students. We&apos;d...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
        <category term="Moodle" scheme="http://www.sixapart.com/ns/types#category" />
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<p>In our Moodle, we will have a collection of courses written by experts and saved as course backups. We want new Moodle users (teachers or managers) to be able to copy these, then add their own teachers and students. We'd be doing this via Web services. But as far as I can see, there is no built-in Web service that copies a course from a backup, and nothing in OKTech's Web services either.</p>

<p>So I need to write my own Web service. Luckily, Rosario Carcò has written a script that restores courses from backups, available at <a href="http://moodle.org/mod/forum/discuss.php?d=128317"><cite>
uploaduser.php enhanced to upload also courses</cite></a>. From it, I see that my Web service will have to call import_backup_file_silently from /backup/lib.php. Its specification being:</p>
<pre>* import_backup_file_silently($pathtofile,
* $destinationcourse,
* $emptyfirst=false,
* $userdata=false,
* $preferences=array())
*
* From the corresponding X-REF:
* this function will restore an entire backup.zip into the specified course
* using standard moodle backup/restore functions, but silently.
* @param string $pathtofile the absolute path to the backup file.
* @param int $destinationcourse the course id to restore to.
* @param boolean $emptyfirst whether to delete all coursedata first.
* @param boolean $userdata whether to include any userdata that may be in the backup file.
* @param array $preferences optional, 0 will be used. Can contain:
* metacourse
* logs
* course_files
* messages
</pre>
So that's what I'll be trying.
</p>]]>
        
    </content>
</entry>

<entry>
    <title>Latest Web-services Demo</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2012/01/-heres-my-latest-version.html" />
    <id>tag:www.j-paine.org,2012:/blog//3.240</id>

    <published>2012-01-09T10:26:00Z</published>
    <updated>2012-01-10T09:44:38Z</updated>

    <summary> Here&apos;s my latest version of my Moodle Web-services demo, which I&apos;ve also posted to Example of using Web services. I&apos;ve added code to parse the XML responses, so that I can extract user data and course data returned by...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
        <category term="Moodle" scheme="http://www.sixapart.com/ns/types#category" />
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<p>
Here's my latest version of my Moodle Web-services demo, which I've also posted to <a href="http://moodle.org/mod/forum/discuss.php?d=193295#p842702"><cite>Example of using Web services</cite></a>. I've added code to parse the XML responses, so that I can extract user data and course data returned by core_user_get_users_by_id and core_course_get_courses. I'm not yet doing this as completely as I should: my code doesn't handle multiple users, and it doesn't convert strings to integers where it should. However, there's enough there for me to check that Moodle has in fact created the users and courses it thinks it has, and for me to get their details one at a time. Note that role assignment and user deletion give spurious error messages (under Moodle 2.2) as I describe at <a href="http://moodle.org/mod/forum/discuss.php?d=193402#p842339"><cite>Am I calling core_role_assign_roles correctly? What do these errors mean?</cite></a>, and that role assignment is giving me other errors that I didn't expect, also as discussed there. I don't yet know whether these are correct.
<pre>
/*
The 'curl' that this uses comes from
https://github.com/moodlehq/sample-ws-clients/blob/master/PHP-REST/curl.php .
*/


/* If true, the program displays each
   XML response returned by calling Moodle.
*/
define( "TRACING", true );


/* Returns a structure defining
   a test user whose name, password, etc. end
   in $n.
*/
function make_test_user( $n ) 
{
  $user = new stdClass();
  $user-&gt;username = 'testusername' . $n;
  $user-&gt;password = 'testpassword' . $n;
  $user-&gt;firstname = 'testfirstname' . $n;
  $user-&gt;lastname = 'testlastname' . $n;
  $user-&gt;email = 'testemail' . $n . '@moodle.com';
  $user-&gt;auth = 'manual';
  $user-&gt;idnumber = 'testidnumber' . $n;
  $user-&gt;lang = 'en';
  $user-&gt;theme = 'standard';
  $user-&gt;timezone = '0';
  $user-&gt;mailformat = 0;
  $user-&gt;description = 'Hello World!';
  $user-&gt;city = 'testcity' . $n;
  $user-&gt;country = 'uk';
  return $user;
}


/* Returns a structure defining
   a test course whose name etc. end
   in $n.

   I have set the category ID to 1.
   This works, but is almost certainly wrong.
   I need to find out what it should be.
*/
function make_test_course( $n ) 
{
  $course = new stdClass();
  $course-&gt;fullname = 'testcourse' . $n;
  $course-&gt;shortname = 'testcourse' . $n;
  $course-&gt;categoryid = 1;
  return $course;
}


/* Creates a user from a
   structure defining a user. If the
   creation succeeds, returns the 
   ID for this user. If not, throws
   an exception whose text is the XML
   returned by Moodle.
*/
function create_user( $user, $token )
{
  $users = array( $user );
  $params = array( 'users' =&gt; $users );

  $response = call_moodle( 'core_user_create_users', $params, $token );

  if ( xmlresponse_is_exception( $response ) )
    throw new Exception( $response );
  else {
    $user_id = xmlresponse_to_id( $response );
    return $user_id;
  }
}


/* Returns a user data structure containing Moodle's
   data for $user_id. It generates this by
   parsing the XML that Moodle returns. If Moodle
   thinks there is no such user, returns NULL.
*/
function get_user( $user_id, $token )
{
  $userids = array( $user_id );
  $params = array( 'userids' =&gt; $userids );

  $response = call_moodle( 'core_user_get_users_by_id', $params, $token );

  $user = xmlresponse_to_user( $response );

  if ( array_key_exists( 'id', $user ) )
    return $user;
  else
    return NULL;
  // If there is no user with this ID, Moodle
  // returns the same enclosing XML as if there were, but 
  // with no values for ID and the other fields. My
  // XML-parsing code therefore creates an object
  // with no fields, which the conditional above
  // detects.
}


/* Deletes the user with ID $user_id. 
   If the delete fails, throws
   an exception whose text is the XML
   returned by Moodle.
*/
function delete_user( $user_id, $token )
{
  $userids = array( $user_id );
  $params = array( 'userids' =&gt; $userids );

  $response = call_moodle( 'core_user_delete_users', $params, $token );

  if ( xmlresponse_is_exception( $response ) )
    throw new Exception( $response );
}


/* Assigns the role with $role_id to the user with $user_id
   in the specified context.

   At the moment, always returns an error. I don't know 
   whether this is a bug in Moodle, or a problem with my
   configuration or user or whatever.
*/
function assign_role( $user_id, $role_id, $context_id, $token )
{
  $assignment = array( 'roleid' =&gt; $role_id, 'userid' =&gt; $user_id, 'contextid' =&gt; $context_id );
  $assignments = array( $assignment );
  $params = array( 'assignments' =&gt; $assignments );

  $response = call_moodle( 'core_role_assign_roles', $params, $token );
}


/* Creates a course from a
   structure defining a course. If the
   creation succeeds, returns the 
   ID for this course. If not, throws
   an exception whose text is the XML
   returned by Moodle.
*/
function create_course( $course, $token ) 
{
  $courses = array( $course );
  $params = array( 'courses' =&gt; $courses );

  $response = call_moodle( 'core_course_create_courses', $params, $token );

  if ( xmlresponse_is_exception( $response ) )
    throw new Exception( $response );
  else {
    $course_id = xmlresponse_to_id( $response );
    return $course_id;
  }
}


/* Returns a course data structure containing Moodle's
   data for $course_id. It generates this by
   parsing the XML that Moodle returns. If Moodle
   thinks there is no such course, returns NULL.
*/
function get_course( $course_id, $token )
{
  $courseids = array( $course_id );
  $params = array( 'options' =&gt; array('ids' =&gt; $courseids ) );

  $response = call_moodle( 'core_course_get_courses', $params, $token );

  $course = xmlresponse_to_course( $response );

  if ( array_key_exists( 'id', $course ) )
    return $course;
  else
    return NULL;
  // If there is no user with this ID, Moodle
  // returns the same enclosing XML as if there were, but 
  // with no values for ID and the other fields. My
  // XML-parsing code therefore creates an object
  // with no fields, which the conditional above
  // detects.
}


/* Enrols the user into the course with the specified role.
   Does not yet check for errors.

   I haven't tested this.
*/
function enrol( $user_id, $course_id, $role_id, $token ) 
{
  $enrolment = array( 'roleid' =&gt; $role_id, 'userid' =&gt; $user_id, 'courseid' =&gt; $course_id );
  $enrolments = array( $enrolment );
  $params = array( 'enrolments' =&gt; $enrolments );

  $response = call_moodle( 'enrol_manual_enrol_users', $params, $token );
}


/* Returns data about users enrolled in the specified course.

   Not sure what Moodle returns yet, so exactly how
   I should parse it. Does not handle multiple users.
*/
function get_enrolled_users( $course_id, $token ) 
{
  $params = array( 'courseid' =&gt; $course_id );

  $response = call_moodle( 'core_enrol_get_enrolled_users', $params, $token );

  $user = xmlresponse_to_user( $response );
  return $user;
}


/* Calls the Moodle at http://moodle.mazegreenyachts.com, invoking the specified
   function on $params. Also takes a token. 
   Returns Moodle's response as a string containing XML.
*/ 
function call_moodle( $function_name, $params, $token )
{
  $domain = 'http://moodle.mazegreenyachts.com';

  $serverurl = $domain . '/webservice/rest/server.php'. '?wstoken=' . $token . '&amp;wsfunction='.$function_name;

  require_once( './curl.php' );
  $curl = new curl;

  $response = $curl-&gt;post( $serverurl . $restformat, $params );

  if ( TRACING ) 
    echo "Response from $function_name: \n", $response, "\n";

  return $response;
}


/* Given a string containing XML returned
   by a successful user creation or course
   creation, parses it and returns the user or course ID
   as an integer.
   Undefined if the XML does not contain such an ID,
   for example if it's an error response.
*/
function xmlresponse_to_id( $xml_string )
{
  $xml_tree = new SimpleXMLElement( $xml_string );          

  $value = $xml_tree-&gt;MULTIPLE-&gt;SINGLE-&gt;KEY-&gt;VALUE;
  $id = intval( sprintf( "%s", $value ) );
  // See discussion on http://php.net/manual/es/book.simplexml.php ,
  // especially the posting for "info at kevinbelanger dot com 20-Jan-2011 05:07".
  // There is a bug in the XML parser whereby it doesn't return the
  // text associated with property [0] of a node. The above
  // posting uses sprintf to force a conversion to string.

  return $id;
}  


/* Given a string containing XML returned
   by a successful call to core_user_get_users_by_id,
   parses it and returns the data as a user
   data structure.
   Undefined if the XML does not contain such an ID,
   for example if it's an error response.

   Does not yet handle fields with multiple values.
   I think these are customfields, preferences,
   and enrolledcourses.
*/
function xmlresponse_to_user( $xml_string )
{
  return xmlresponse_parse_names_and_values( $xml_string );
}


/* Given a string containing XML returned
   by a successful call to core_course_get_courses,
   parses it and returns the data as a course
   data structure.
   Undefined if the XML does not contain such an ID,
   for example if it's an error response.

   Does not yet handle fields with multiple values.
*/
function xmlresponse_to_course( $xml_string )
{
  return xmlresponse_parse_names_and_values( $xml_string );
}


/* This parses a string containing the XML returned by
   functions such as core_course_get_courses,
   core_user_get_users_by_id, or core_enrol_get_enrolled_users.
   These strings contain name-value pairs encoded thus:
     &lt;RESPONSE&gt;
     &lt;MULTIPLE&gt;
     &lt;SINGLE&gt;
     &lt;KEY name="id"&gt;&lt;VALUE&gt;169&lt;/VALUE&gt;
     &lt;/KEY&gt;
     &lt;KEY name="username"&gt;&lt;VALUE&gt;testusername32&lt;/VALUE&gt;
     &lt;/KEY&gt;
     &lt;/SINGLE&gt;
     &lt;/MULTIPLE&gt;
     &lt;/RESPONSE&gt;
   The function returns an object with the corresponding
   keys and values.

   Does not yet convert strings to integers where they
   ought to be converted.
*/
function xmlresponse_parse_names_and_values( $xml_string )
{
  $xml_tree = new SimpleXMLElement( $xml_string ); 

  $struct = new StdClass();

  foreach ( $xml_tree-&gt;MULTIPLE-&gt;SINGLE-&gt;KEY as $key ) {
    $name = $key['name'];
    $value = (string)$key-&gt;VALUE;
    $struct-&gt;$name = $value;
  }

  return $struct;
}


/* True if $xml_string's top-level is
   &lt;EXCEPTION&gt;. I use this to check for error
   responses from Moodle.
*/
function xmlresponse_is_exception( $xml_string )
{
  $xml_tree = new SimpleXMLElement( $xml_string );          

  $is_exception = $xml_tree-&gt;getName() == 'EXCEPTION';
  return $is_exception;
}  


/* These are some role IDs from our Moodle,
   obtained by querying the database with
   the command
     select * from mdl_role;
   Hopefully, Moodle won't change them.
   There are a few other roles, but not ones
   I think we need.
*/
define( "MANAGER_ROLE_ID", 1 );
define( "COURSE_CREATOR_ROLE_ID", 2 );
define( "TEACHER_ROLE_ID", 3 );
define( "NON_EDITING_TEACHER_ROLE_ID", 4 );
define( "STUDENT_ROLE_ID", 5 );
define( "GUEST_ROLE_ID", 6 );
define( "AUTHENTICATED_USER_ROLE_ID", 7 );
define( "AUTHENTICATED_USER_ON_FRONTPAGE_ROLE_ID", 8 );


/* These are some context IDs from our Moodle,
   obtained by querying the database with
   the command
     select * from mdl_context;
   and by reading http://moodle.org/mod/forum/discuss.php?d=60125 ,
   "Roles and contexts in Moodle 1.7".
   Hopefully, Moodle won't change them.
   There are other contexts, but not ones
   I think we need.
*/
define( "SYSTEM_CONTEXT_ID", 1 );


function demo()
{
  try {
    echo "Demo of Moodle Web services\n";
    echo "===========================\n";

    $token = '9bbdcddccb00480ce6c87fa8358c4a54';
    echo "\nUses this token which I created manually: " . $token . "\n"; 

    echo "\nWill now create two users from the following data:\n";
    $user_suffix = 38;
    $user_data_1 = make_test_user( $user_suffix );
    print_r( $user_data_1 );
    echo "\n";

    $user_data_2 = make_test_user( $user_suffix+1 );
    print_r( $user_data_2 ); 
    echo "\n";

    $user_id_1 = create_user( $user_data_1, $token );
    if ( is_null( get_user( $user_id_1, $token ) ) )
      echo "\nUser 1 doesn't seem to have been created\n";
    else
      echo "\nUser 1's ID = " . $user_id_1 . "\n";

    $user_id_2 = create_user( $user_data_2, $token );
    if ( is_null( get_user( $user_id_2, $token ) ) )
      echo "\nUser 2 doesn't seem to have been created\n";
    else
      echo "\nUser 2's ID = " . $user_id_2 . "\n";

    echo "\nWill now assign a role to make user 1 a student:\n";
    
    assign_role( $user_id_1, STUDENT_ROLE_ID, SYSTEM_CONTEXT_ID, $token );

    echo "\nWill now get the user details using user 1's ID:\n";
    $user_data_from_moodle_1 = get_user( $user_id_1, $token );
    echo "\nUser details:\n";
    print_r( $user_data_from_moodle_1 );

    echo "\nFor comparison, will now get details for a non-existent user:\n";
    $user_data_from_moodle_3 = get_user( 9999, $token );
    echo "\nUser details:\n";
    print_r( $user_data_from_moodle_3 );

    echo "\nWill now create a course from the following data:\n";
    $course_suffix = 127;
    $course_data = make_test_course( $course_suffix );
    print_r( $course_data ); 
    echo "\n";

    $course_id = create_course( $course_data, $token );
    if ( is_null( get_course( $course_id, $token ) ) )
      echo "\nCourse doesn't seem to have been created\n";
    else
      echo "\nCourse ID = " . $course_id . "\n";

    echo "\nWill now get the course details using that ID:\n";
    $course_data_from_moodle = get_course( $course_id, $token );
    echo "\nCourse details:\n";
    print_r( $course_data_from_moodle );

    echo "\nFor comparison, will now get details for a non-existent course:\n";
    $course_data_from_moodle_3 = get_course( 9999, $token );
    echo "\nCourse details:\n";
    print_r( $course_data_from_moodle_3 );

    echo "\nWill now enrol users 1 and 2:\n";
    $role_id = STUDENT_ROLE_ID;
    enrol( $user_id_1, $course_id, $role_id, $token );
    enrol( $user_id_2, $course_id, $role_id, $token );

    echo "\nWill now get the IDs of who Moodle thinks is enrolled:\n";
    $users_in_course = get_enrolled_users( $course_id, $token );
    echo "\nUser details:\n";
    print_r( $users_in_course );

    echo "\nWill now delete the users:\n";
    delete_user( $user_id_1, $token );
    if ( ! is_null( get_user( $user_id_1, $token ) ) )
      echo "\nUser 1 doesn't seem to have been deleted\n";
    else
      echo "\nUser 1 deleted\n";

    delete_user( $user_id_2, $token );
    if ( ! is_null( get_user( $user_id_2, $token ) ) )
      echo "\nUser 2 doesn't seem to have been deleted\n";
    else
      echo "\nUser 2 deleted\n";
  } 
  catch ( Exception $e ) {
    echo "\nCaught exception:\n" .  $e-&gt;getMessage() . "\n";
  }
}


demo();
</pre>
</p>]]>
        
    </content>
</entry>

<entry>
    <title>First Experiment with Moodle Web Services</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2012/01/first-experiment-with-moodle-web-services.html" />
    <id>tag:www.j-paine.org,2012:/blog//3.242</id>

    <published>2012-01-04T19:00:00Z</published>
    <updated>2012-01-10T09:55:42Z</updated>

    <summary> I&apos;ve just posted my first experiment with Moodle Web services to Example of using Web services. It&apos;s a PHP program that I&apos;ve written while teaching myself about Web services. It contains functions that encapsulate adding a user, getting user...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
        <category term="Moodle" scheme="http://www.sixapart.com/ns/types#category" />
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<p>
I've just posted my first experiment with Moodle Web services to
<a href="http://moodle.org/mod/forum/discuss.php?d=193295"><cite>
Example of using Web services</cite></a>. It's a PHP program that I've written while teaching myself about Web services. It contains functions that encapsulate adding a user, getting user details from an ID, deleting a user, assigning a rôle, creating a course, and enroling a user in a course. The end of the program calls these one by one, displaying its progress as it goes. I use XML, and call the PHP SimpleXml parser to distinguish error responses, and to extract the ID from newly created users and courses. 
</p>]]>
        
    </content>
</entry>

<entry>
    <title>Cartoons at the Said</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/11/c.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.237</id>

    <published>2011-11-22T11:38:31Z</published>
    <updated>2011-11-22T11:38:48Z</updated>

    <summary> Here&apos;s a portfolio of my cartoons in Word, with explanations of the technical ones. I gave out copies at the East Oxford Drawing Collective&apos;s exhibition in the Said Business School....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
Here's a <A HREF="http://www.j-paine.org/dobbs/said.doc">portfolio</A> of my cartoons in Word, with explanations of the technical ones. I gave out copies at the <A HREF="http://www.oxfordartcat.com/2011/11/east-oxford-drawing-collective-take.html">East Oxford Drawing Collective's exhibition in the Said Business School</A>.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>V.I.P Briggs</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/10/vip-briggs.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.225</id>

    <published>2011-10-30T18:14:50Z</published>
    <updated>2011-10-31T01:05:21Z</updated>

    <summary> A caricature I drew of an acquaintance&apos;s wife. She&apos;s a social worker....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<A HREF="http://www.j-paine.org/dobbs/vip_briggs_451.gif"><IMG SRC="http://www.j-paine.org/dobbs/vip_briggs_310.gif"
ALT="Cartoon of lady wearing 1980s-style power suit and a badge saying 'VIP'. She carries a huge key-ring, and is wagging a finger and saying 'You've been very naughty. I'm going to have to SMACK you.' The cartoon is captioned 'V.I.P. Briggs, Senior Manager.'"
></A>
</P>

<P>
A caricature I drew of an acquaintance's wife. She's a social worker.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>They Found the Grail in Scope</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/10/th.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.227</id>

    <published>2011-10-30T17:35:29Z</published>
    <updated>2011-10-31T01:01:23Z</updated>

    <summary> I drew this for a charity card sale. Selling my cards for Scope in Summertown, and giving them half the profit....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<A HREF="http://www.j-paine.org/dobbs/grail_680.gif"><IMG SRC="http://www.j-paine.org/dobbs/grail_429.gif"
ALT="Cartoon of knights finding Holy Grail in Summertown Scope shop. The grail gleams at the end of a shelf, next to a coffee cup, a travel clock, a photo of a flower, a straw donkey souvenir of España, and a china cat. The knights converse as follows. A: 'The Holy Grail! We've found it!' B: '£9.99 is a lot of money.' C: 'Cheaper than that one in Oxfam.' D: 'Wonder who gave it?' E:'They do say Summertown is rich.' F (sniffing disdainfully): 'Rich beyond the dreams of avarice.'"></A>
</P>

<P>
I drew this for a charity card sale. Selling my cards for Scope in Summertown, and giving them half the profit.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>BB Sheep One</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/10/b.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.229</id>

    <published>2011-10-30T16:00:00Z</published>
    <updated>2011-10-31T02:27:35Z</updated>

    <summary> A birthday card I drew for a friend who likes wind turbines....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<A HREF="http://www.j-paine.org/dobbs/sheep_1275.gif"><IMG SRC="http://www.j-paine.org/dobbs/sheep_510.gif"
ALT="Cartoon with three frames. First frame shows a sheep standing on a hillside, with wind turbines whirring away in the background. The sheep looks a bit angry and has a thought bubble saying 'Bored'. Second frame shows the sheep with a thought bubble enclosing a light-bulb with the word 'IDEA!' nearby. Third frame shows the sheep contentedly chewing grass and watching a television with a power cable running to one of the turbines. The television screen says 'BB Sheep One'." 
></A>
</P>

<P>
A birthday card I drew for a friend who likes wind turbines.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>Tommy Guthrie</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/10/tommy-guthrie.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.233</id>

    <published>2011-10-24T01:21:12Z</published>
    <updated>2011-10-31T02:21:29Z</updated>

    <summary> Quick caricature of busker in Cornmarket. I originally used these words, from Hobo&apos;s Lullaby: I know the police cause you trouble They cause trouble everywhere But when you die and go to Heaven You&apos;ll find no policemen there. but...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<IMG SRC="http://www.j-paine.org/dobbs/guthrie_304.gif"
ALT="Roughish caricature of guitarist in front of microphone, with drum displaying 'The Tommy Guthrie One-Man Band'. The guitarist is singing 'I heard a siren / Coming from the docks / Saw a train / Set the night on fire.'">
</P>

<P>
Quick caricature of busker in Cornmarket. I originally used these words, from <CITE>Hobo's Lullaby<CITE>: 
<BLOCKQUOTE>
I know the police cause you trouble<BR>
They cause trouble everywhere<BR>
But when you die and go to Heaven<BR>
You'll find no policemen there.
</BLOCKQUOTE>
but he thought that might be provocative.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>Ulfgar Moves With the Times</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/10/u.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.231</id>

    <published>2011-10-24T00:53:58Z</published>
    <updated>2011-10-31T02:07:32Z</updated>

    <summary> A rough sketch drawn at the Wolvercote farmers&apos; market last Sunday. It was their 9th birthday, so my mind was running on anniversaries....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P><IMG SRC="http://www.j-paine.org/dobbs/witches_510.gif"
ALT="Rough sketch of no-entry sign enclosing a witch being burnt at a stake. Under the sign is a caption: 'We don't burn witches in Wolvercote.' and then a newline and another caption: 'Wolvercote ~ proud to enter the 21st century.' with '21st' crossed out and replaced by  '18th'."
></P>

<P>
A rough sketch drawn at the Wolvercote farmers' market last Sunday. It was their 9th birthday, so my mind was running on anniversaries.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>The Last of the Busketeers</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/08/the-last-of-the-busketeers.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.222</id>

    <published>2011-08-21T03:17:29Z</published>
    <updated>2011-08-21T03:53:45Z</updated>

    <summary> I didn&apos;t think I&apos;d end up sketching environmental-health officers the day before yesterday, but I happened to be practising my drawing when they, and the police, moved in on a band called The Two Busketeers, playing in Cornmarket outside...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<A HREF="http://www.j-paine.org/dobbs/lastofbusketeers_660.gif"><IMG SRC="http://www.j-paine.org/dobbs/lastofbusketeers_520.gif" 
ALT="Sketch of an Oxford environmental-health officer, surrounded by snatches of the things she was saying to the band The Two Busketeers"
></A>
</P>

<P>
I didn't think I'd end up sketching environmental-health officers the day before yesterday, but I happened to be practising my drawing when they, and the police, moved in on a band called <A HREF="http://www.myspace.com/thetwobusketeers">The Two Busketeers</A>, playing in Cornmarket outside HSBC. Three police whose numbers &mdash; 7467, 3190, and 4988 &mdash; I saw, and perhaps another whose I didn't. This seemed excessive.This is a quick sketch of the environmental-health officer who was doing most of the speaking, together with some of what she was saying:
</P>

<P>
&mdash; There's people working here who don't need to hear your noise.<BR>

&mdash; Don't you think the police have enough to do?<BR>

&mdash; The rules change, unfortunately. It's your responsibility. We do everything we can. If you choose not to read it, that's your matter. It's your attitude, isn't it?<BR>

&mdash; If you display printed matter, that's an &pound;80 fine for distribution of printed matter.<BR>

&mdash; If you give the CDs away, that goes under the distribution of free printed matter.<BR>

&mdash; If you ask for donations, that's vagrancy, which is begging.<BR>

&mdash; I'm in the city centre every day. If I see <I>anything</I> untoward.<BR>

&mdash; <I>And</I> I can tell you that <I>all</I> the busking spots are taken!
</P>

<P>
It's a rough sketch, and shaky, but I wasn't entirely confident that she wouldn't have me arrested. Perhaps for free distribution of unprinted matter.
What was distasteful was the glee that the officers and police seemed to take in telling the band that they weren't allowed to sell CDs, ask for donations towards CDs, give away CDs, or even show their business cards. For example. "There's people working here who don't need to hear your noise". OK, perhaps the officer didn't like the music, or did believe it was too loud. But calling it noise was gratuitous. And there seemed to be more than the legally-required amount of schadenfreude in her final retort: "And I can tell you that all the busking spots are taken!"
</P>

<P>
Band member to policeman: "It's bloody ridiculous". Police to band member: "I could arrest you right now for your attitude". As a South Korean friend of mine, who grew up under the dictatorship, said: "The band were only expressing their frustration. Surely it's against human rights to arrest someone because they complain".
</P>

<P>
I've enjoyed listening to buskers, and buying their CDs, in Verona, Eindhoven, Maastricht, Paris, Poitiers, Aachen, and Heidelberg. It seems a shame that tourists from these and other cities won't now be able to get the same enjoyment in Oxford.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>Down the Tree</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/07/down-the-tree.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.220</id>

    <published>2011-07-29T02:37:31Z</published>
    <updated>2011-07-29T02:42:28Z</updated>

    <summary> In Oxford, Summertown is regarded as &quot;posh&quot;. Cowley Road is not....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<IMG SRC="http://www.j-paine.org/dobbs/stone_age_473.gif"
ALT="Cartoon of a stona-age lady, with club and animal skin, handing flowers shyly to snooty, dismissive-looking man. The man is saying 'It could never work. I'm from Summertown, and you're from Cowley Road.'">
</P>

<P>
In Oxford, Summertown is regarded as "posh". Cowley Road is not.
</P>]]>
        
    </content>
</entry>

<entry>
    <title>Bun Fight</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/07/bun-fight.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.219</id>

    <published>2011-07-29T02:24:18Z</published>
    <updated>2011-07-29T02:33:48Z</updated>

    <summary> I drew this to be presented to Oxford&apos;s Lord Mayor, Elise Benjamin, on her visit to East Oxford Farmers&apos; Market to commemorate its fifth birthday, 23rd July 2011....</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<P>
<A HREF="http://www.j-paine.org/dobbs/cakes_846.gif"><IMG SRC="http://www.j-paine.org/dobbs/cakes_540.gif"
ALT="Three women at cake stalls in a farmers' market. The first and third are pelting one another with cakes, while the second cowers under her table. The caption reads 'You can never have too many cake stalls.'"></A>
</P>

<P>
I drew this to be presented to Oxford's Lord Mayor, Elise Benjamin, on her visit to East Oxford Farmers' Market to commemorate its fifth birthday, 23rd July 2011. 
</P>]]>
        
    </content>
</entry>

<entry>
    <title>Ulfgar&apos;s Island</title>
    <link rel="alternate" type="text/html" href="http://www.j-paine.org/blog/2011/04/ulfgar-alone.html" />
    <id>tag:www.j-paine.org,2011:/blog//3.216</id>

    <published>2011-04-27T12:10:52Z</published>
    <updated>2011-04-27T12:21:52Z</updated>

    <summary> I drew this as an Easter card for Carl and Katie in the Post Box shop in Wolvercote. It&apos;s about the three-tonne weight limit that may be imposed on the old Goose Green bridge in Wolvercote, to keep the...</summary>
    <author>
        <name>Jocelyn Ireson-Paine</name>
        <uri>http://www.j-paine.org</uri>
    </author>
    
    
    <content type="html" xml:lang="en" xml:base="http://www.j-paine.org/blog/">
        <![CDATA[<p>
<img src="http://www.j-paine.org/dobbs/bridge_460.gif" 
alt="Dad in sitting room to little boy who is smeared with chocolate and surrounded by bits of Easter egg: 'Don't eat so many Easter eggs, or they won't let you cross the bridge.'">
</p>

<p>
I drew this as an Easter card for Carl and Katie in the Post Box shop in Wolvercote. 
It's about the <A HREF="http://www.oxfordmail.co.uk/news/8972321.Villagers__weight_worry_over_bridge/">three-tonne weight limit</A> that may be imposed on the old Goose Green bridge in Wolvercote, to keep the railway underneath safe. The council says it can no longer afford to rebuild so the bridge can carry up to 44 tonnes.
</p>]]>
        
    </content>
</entry>

</feed>

