You are currently browsing the tag archive for the ‘moodle’ tag.

I went to a great event on this leap day at Senate House ‘Moodle 2: Making the leap’ organised by JISC Regional Support Centre. We heard pre/during/post migration experiences from 4 FE colleges who had made the move to Moodle 2.x from Moodle 1.x.

The most striking thing about their stories, and indeed from talking to others over coffee, was the diversity of setups both technical and organisational. The possible combinations of { virtual learning environment + student record system + database platform + ePortfolio } must surely be infinite! As well as that we also saw a range of uses for Moodle from straightforward course delivery to staff CPD to use as a student portal/intranet.

Why were we there? It was all about migration from Moodle 1 to 2 – how to do it? when to do it? what can go wrong? how to engage academics and students?

How to do it?

There are 2 basic approaches here:

  • upgrade installation in-situ
    This involves replacing the Moodle code old with new and letting Moodle do the upgrade – either via the interface or command line. This seems to be a better option for those with an externally hosted Moodle where there are relatively few code customisations or integrations. The further away your Moodle is from a ‘vanilla’ Moodle the harder this may be so watch out!
  • start afresh and populate via import
    Here a parallel Moodle installation is set up anew and courses are exported from old Moodle and imported to new. At one place a team of 8 staff manually migrated 471 courses in 2 days. This allows an opportunity to clear out old data but users and enrolments will need to be imported separately. When ready you simply need to change DNS settings to point your Moodle URL to the new installation IP address. You can keep the old one as an archive.

The interesting thing here is that there is no clear winner – it will depend very much on your setup and institutional policies on use of VLE. Some places insist on courses being rebuilt every year so for them the fresh install would be the way ahead. Either way both will probably involve a few days (weeks?) downtime while the work is carried out.

When to do it?
The resounding message here was ‘do it early’, probably before your academics go off for their summer hols – especially if you’re expecting them to check and develop their courses in the new Moodle. The start of the academic year is stressful enough so I can definitely see the logic here.

What can go wrong?
Lots! Or maybe nothing. Who knows? It really is a bit like that so testing is the order of the day. For the upgrade option make a duplicate site (and database of course) to test the upgrade process – see what borks it and fix it in your real installation. It could be something as simple as a dodgy filename buried somewhere in the content or rogue code in a label somewhere (paste from MS Word has a lot to answer for!) When testing try and replicate your original server architecture/platform as near as possible.

One college reported that 30% of courses failed to appear in their upgraded version! Whilst you may be able to fix this manually using an army of low-paid summer jobbers (as another FE did) if you are migrating 10,000+ courses then things start to get tricky!

If you have used 3rd party plugins in Moodle 1.9x and these don’t/won’t exist for Moodle 2.x then obviously you will lose this functionality so look at alternatives well ahead of time. The second migration option won’t carry over any users or user data (including glossary data) so you’ll need to deal with that.

How to engage academics and students?
The first step might be to hold mass-inductions (makes me think of Moodle as some kind of cult – maybe it is!) to show the wonders of Moodle2 followed by more personal and practical sessions with staff to cover the areas of major change (file management, navigation, blocks, quiz questions).

Another approach is to get the early-adopters to go over first and show the rest that the water’s lovely, come on in. For students it seems a stealth approach is best – as long as it works and does what they expect they don’t really care what version of Moodle you’re running.

The introduction of a new version of Moodle was used by some as a chance to reinforce the pedagogic principles of the VLE use while for others it was combined with a new swanky theme – you’re going to have to rebuild your theme anyway so why not?

Finally … incentivise! Several FEs who presented today use Moodle medals to award courses bronze, silver and gold standard. Any courses that didn’t meet at least bronze standard were not migrated – harsh but a good incentive for teachers to start engaging. One FE go even further and offer a Level 4 qualification in VLE use.

So, thanks to all the speakers, JISC RSC, ULCC and all people I ‘networked’. The emphasis was on sharing and helping and I personally found it a really useful day – I came away with a to-do list with over 30 ideas so watch out City!

    Advertisements

Where did the last month go!? As explained in an earlier post Moodle URLs don’t make for very good tracking using traditional server logs or Google Analytics (GA). We have implemented a fix using pseudo-URLs for GA and here I’ll show the basics of code we’ve used (minus some institution-specific tweaks).

We’re aiming to create a pseudo-URL similar to:
{school}.{role}.{course}.{activity-action}.{id}

All the code is in a section in the head part of our theme’s header.html (Moodle 1.9x) or layout/general.php etc (Moodle 2.x) plus any other layout pages that you want to track. Actually, the code is stored in an include and just included where needed.

To set the scene, Moodle authenticates against Active Directory and gets some data from this into the global $USER on login. So we need this:

  // get user department code
  if (isset($USER->department)) {
    $userDept =   trim($USER->department);
  } else { 
    $userDept = ''; 
  }

Next, we have a bit of code that sorts these department codes into school codes ($userSchool). I won’t show these but they result in values like CASS, SOI, SEMS etc. You could just as easily set up a variable here that gets the category code, like this:

  // get category ID
  $category = $COURSE->category;

Getting the user’s role in context is a bit trickier, we’re really only interested in the highest level role at course context level for our purposes but I suppose you could adapt to get a role for the activity context:

  // get role of user for the course context (level=50)   
  $gaUserID = $USER->id;
  $gaRole = 'unknownrole'; //default
  if ($gaUserID > 0) { 
    // get contextID for this course first
    $gaQry = get_record('context','instanceid',$COURSE->id,'contextlevel','50');
    $gaContextID = $gaQry->id; 
    // then count records to see if user has an entry in role_assignments table
    $gaQryCnt = count_records('role_assignments','contextid',$gaContextID,'userid',$USER->id);
    // note a user may have multiple roles in db so get highest one  
    if ($gaQryCnt > 0) { // ok, there are entries so get role
      $gaQry = get_records_sql("SELECT * FROM m_role_assignments WHERE contextid='" . $gaContextID . "' AND userid='" . $USER->id . "' ORDER BY roleid ASC",0,1);
      $gaQryObj = $gaQry[key($gaQry)]; // get key for 1st element in associative array
      $gaQry = get_record('role','id',$gaQryObj->roleid);  // get role name
      $gaRole = $gaQry->shortname;
    } else { // no specific course role, then what are they then?
      if ($gaUserID == 1) $gaRole = 'guest';  // assuming standard Moodle setup!
      if ($gaUserID == 2) $gaRole = 'superadmin';  // assuming standard Moodle setup!
      // is user a Moodle admin?
      $gaQryCnt2 = count_records('role_assignments','contextid',1,'userid',$USER->id);
      if ($gaQryCnt2 > 0) $gaRole = 'superadmin';
    }
  } else {
    $gaRole = 'notloggedin';
  }

Now, to get the module shortname … this bit’s easy:

  // get course shortname, could also use id, fullname, 
  // idnumber although latter not always given
  $gaCourse =  $COURSE->shortname;

Next we need the activity type and the action being undertaken, this could be split into several variables if required but we are happy with values like: mod-resource-view.

Getting useful info here required a lot of digging into how various activities constructed URLs, there is a lot of variation and custom request variables which are not all covered here (e.g. Turnitin):

  // work out what major task is being done by looking at raw URL
  $gaPagePath = $CFG->pagepath;
  $gaTask = $gaPagePath;
  $gaURLParts = explode('/',$gaPagePath); 
  $gaURLPartsCount = count($gaURLParts); 
  // we're interested in array elements 3 and above, the bits after Moodle root URL
  // so if URL is http://moodle.college.ac.uk/course/view.php?id=102
  // element 3 = 'course', 4 = 'view.php'   (except for some admin pages, see below)
  // following bit checks down the path and grabs relevent keywords to build task
  if ($gaURLPartsCount > 3) {
    $gaTask = $gaURLParts[3];
    if ($gaTask == '') $gaTask = 'home';
  }
  if ($gaURLPartsCount > 4) { 
    if ($gaURLParts[4] != '') {
      if (strpos($gaURLParts[4],'.') > 0) {
        $gaTask = $gaTask . '-' . substr($gaURLParts[4],0,strpos($gaURLParts[4],'.'));
      } else {
      $gaTask = $gaTask . '-' . $gaURLParts[4];
      }
    }
  }
  if ($gaURLPartsCount > 5) {
    if ($gaURLParts[5] != '') {
      if (strpos($gaURLParts[5],'.') > 0) {
        $gaTask = $gaTask . '-' . substr($gaURLParts[5],0,strpos($gaURLParts[5],'.'));
      } else {
        $gaTask = $gaTask . '-' . $gaURLParts[5];
      }
    }
  }
  
  // fix for moodle bug where admin urls are not getting reported correctly in $CFG->pagepath
  // http://tracker.moodle.org/browse/MDL-20342
  $gaTask = str_replace('/','-',$gaTask);

  // fix for help
  if ($gaTask == 'help.php') $gaTask = 'help'; 
                                    
  // if there is a current action specified then append that to task but only for non admin stuff
  if ($gaURLParts[0] != 'admin') {
    if (isset($_REQUEST['currentaction'])) $gaTask = $gaTask . '-' . $_REQUEST['currentaction'];
    // if there is a section specified then append that to task
    if (isset($_REQUEST['section'])) $gaTask = $gaTask . '-' . $_REQUEST['section'];
    // if there is an action specified then append that to task
    if (isset($_REQUEST['action'])) $gaTask = $gaTask . '-' . $_REQUEST['action'];
    if ( (isset($_REQUEST['frameset'])) && (isset($_REQUEST['page'])) ) $gaTask = $gaTask . "-ims-p" . $_REQUEST['page'];
  }

Now, the last part of the URL, the task or page ID:

  // now get task or page ID however best, this list not exhaustive
  $gaTaskID = '';
  // get id number if page has id=xx argument
  if (isset($_REQUEST['id'])) $gaTaskID = $_REQUEST['id'];
        
  // if $gaTaskID still 0 try looking for mod id in URL
  // e.g. mod/forum/post.php?forum=7 or admin/roles/assign.php?contextid=27
  // note that these URLS work best for course content related stuff not user or admin stuff
  // this list is not exhaustive, just the ones I managed to identify
  if ($gaTaskID == '') {
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'forum') && (isset($_REQUEST['f']))) $gaTaskID =  $_REQUEST['f'];if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'forum') && (isset($_REQUEST['f']))) $gaTaskID =  $_REQUEST['f'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'glossary') && (isset($_REQUEST['g']))) $gaTaskID =  $_REQUEST['g'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'forum') && (isset($_REQUEST['forum']))) $gaTaskID =  $_REQUEST['forum'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'resource') && (isset($_REQUEST['r']))) $gaTaskID =  $_REQUEST['r'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'quiz') && (isset($_REQUEST['cmid']))) $gaTaskID =  $_REQUEST['cmid'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'quiz') && (isset($_REQUEST['q']))) $gaTaskID =  $_REQUEST['q'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'calendar') && (isset($_REQUEST['course']))) $gaTaskID =  $_REQUEST['course'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'user') && (isset($_REQUEST['contextid']))) $gaTaskID =  $_REQUEST['contextid'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'blog') && (isset($_REQUEST['filterselect']))) $gaTaskID =  $_REQUEST['filterselect'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'blog') && (isset($_REQUEST['postid']))) $gaTaskID =  $_REQUEST['postid'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'blog') && (isset($_REQUEST['userid']))) $gaTaskID =  $_REQUEST['userid'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'help.php') && (isset($_REQUEST['file'])) && (isset($_REQUEST['module']))) $gaTaskID =  $_REQUEST['module'] . '/' . $_REQUEST['file'];
  }
  // still no taskID? try update=xx from editing resource
  if ( ($gaTaskID == '') && (isset($_REQUEST['update']))) $gaTaskID = $_REQUEST['update'];
        
  // if still no taskID then set to 0
  if ($gaTaskID == '') $gaTaskID = '0';

Finally, construct the pseudo-URL and set GA account #:

  // now construct pseudo URL using . separator    
  $pseudoUrl = $userSchool . '.' . $gaRole . '.' . $gaCourse . '.' . $gaTask . '.' . $gaTaskID;
  $pseudoUrl = strtolower($pseudoUrl); // all lower case for neatness
  $gaAccount = "UA-xxxxxxxx-1"; // enter your GA account code here

PHP stuff over, in the html part of the header

    <script>     
      // Google Analytics code as a function instead of inline
      function gaSSDSLoad (acct,pseudo_url) {  
        var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
        var s,pageTracker;  
        s = document.createElement('script');  
        s.src = gaJsHost + 'google-analytics.com/ga.js';  
        s.type = 'text/javascript';  
        s.onloadDone = false;  
        function init () { 
          try { pageTracker = _gat._getTracker(acct); } catch (err) {}
          pageTracker._trackPageview(pseudo_url);
        }
        s.onload = function () {    
          s.onloadDone = true;    
          init();  
        };
        s.onreadystatechange = function() {    
          if (('loaded' === s.readyState || 'complete' === s.readyState) && !s.onloadDone) {      
            s.onloadDone = true;      
            init();    
          }  
        };   
      document.getElementsByTagName('head')[0].appendChild(s);
      }  
     
      // now run script using GA account # and pseudo_url
      // fix for IE8
      if(window.addEventListener) {
        /* W3C method. */
        window.addEventListener('load', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") }, false);
      } else if(window.attachEvent) {
        /* IE method. */
        window.attachEvent('onload', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") });
      } else {
        /* Old school method. */
        window.onload = function() {gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>"); };
      }
    
    </script> 

File attached with code, don’t forget to edit GA account #
code_snippet

When we started with Moodle at City back in 2009 we wanted to use Google Analytics (GA) to augment the stats that Moodle collects itself. Partly this is because user stats are a bit hard to get out of Moodle in a flexible and visually attractive way. True, you could deploy custom queries to the back-end database but we wanted to leverage the power of GA. Trouble is, GA uses as its base unit the URL, and Moodle’s URLs are not very informative.

We want to be able to look at stats for different type of activities and user role (e.g. student vs teacher), and to be able to differentiate between schools (and even departments). This just isn’t possible with the standard moodle/mod/{mod-name}/{action}.php?id={id} format of Moodle URLs – all this gives you is the ability to breakdown stats by activity type alone.

What we’re aiming for then is to send to GA some kind of pseudo-URL that contains the information we need to be able to analyse. A model for this pseudo-URL might look something like this:

{school}.{role}.{module}.{activity-action}.{id}

However an example URL from Moodle might look like this:

http://moodle.city.ac.uk/mod/turnitintool/view.php?id=123456

this tells GA nothing about the page other than it is a Turnitin assignment.

Using custom php in the Moodle theme header.html we can construct our pseudo-URL and send this to Google instead of the actual URL.

The URL we generate will look something like this:

law.student.gen_law_imm_2011-12.mod-turnitintool-view.123456

so now we can interrogate our analytics for school*, role, module, activity and action, down to an individual activity if we want but usually aggregated to some degree. There is also scope to extend this, for example, the Turnitin tool uses another request variable &do (values such as “intro” & “submissions”) which could tell GA a bit more about what is actually happening if it was included in the pseudo-URL. Other plugins and activities have other similarly useful request variables in their URLs.

[*Where do we get school from? Our user table grabs school and department codes from our user database on login via LDAP so it is available in the global $USER]

I won’t show the code we used to create the pseudo-URL (if anyone wants that see this post) but the adapted GA script from the header.html <head> section  is here **:

<?php
  $pseudoURL = "blah"; // insert code here to build pseudo-URL
  $gaAccount = "UA-xxxxxxxx-1"; // enter your GA account code here
?>
<script>
// Google Analytics code as a function instead of inline
  function gaSSDSLoad (acct,pseudo_url) {
    var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." :
"http://www.");
    var s,pageTracker;
    s = document.createElement('script');
    s.src = gaJsHost + 'google-analytics.com/ga.js';
    s.type = 'text/javascript';
    s.onloadDone = false;
    function init () {
      try { pageTracker = _gat._getTracker(acct); } catch (err) {}
    pageTracker._trackPageview(pseudo_url);
  }
  s.onload = function () {
    s.onloadDone = true;
    init();
  };
  s.onreadystatechange = function() {
    if (('loaded' === s.readyState || 'complete' === s.readyState) &&
!s.onloadDone) {
      s.onloadDone = true;
      init();
    }
  };
document.getElementsByTagName('head')[0].appendChild(s);
}

// now run script using GA account # and pseudo_url
// fix for IE8
if (window.addEventListener) {
  /* W3C method. */
  window.addEventListener('load', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") }, false);
} else if (window.attachEvent) {
  /* IE method. */
  window.attachEvent('onload', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") });
} else {
  /* Old school method. */
  window.onload = function() {gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>"); };
}

[**Not sure who to credit this to, it wasn’t me, someone in the webteam came up with this before I got here.]

In a future post I’ll talk about how we use Google Analytics and Moodle’s own stats here at City.

I am Mike Hughes, the opinions expressed on this blog are my own and not those of my employer ... do I really need to say this?

Enter your email address to follow this blog and receive notifications of new posts by email.

recent tweets