CakePHP-Mailchimp-datasource 1.3 Read Method

If anyone’s still using CakePHP v1.3 of this brilliant datasource which allows you to treat data on subscribers in MailChimp as if they were in a local model, I had to make a few changes to make it work with v1.3 of the MailChimp API.

 
function read($model, $queryData = array()) {
 $url = $this->buildUrl('listMemberInfo', $queryData['conditions']['emailaddress']);
 $response = json_decode($this->connection->get($url), true);
 if(isset($response['errors'])&&$response['errors']>0) { //this is how errors are indicated
 return false;
 }
 return $response['data']; //allows find('first') to return $response['data'][0]
 }

CakePHP not saving to new field on production server

I wasted a few hours on Saturday morning struggling with the fact that CakePHP on my production server refused to save to a newly added field on a table in my database, while everything worked beautifully on my dev server. After hours of debugging on the live server I suddenly remembered that, with:
Configure::write('debug', 0);
in core.php, as it should be on a production system, Cake uses the cached model definitions in tmp\cache\models and would therefore ignore any changes in the underlying database until these were refreshed. Simply either:

  1. Change temporarily to Configure::write('debug', 2); and then run your code before changing it back again or;
  2. Delete the contents of tmp\cache\models

and it will pick up your new field.

Timezone problems/ time offset in PHP under IIS/Windows

We’ve been struggling for a while now with an unexplained three hour offset in log files and error messages created by QuestionMark Perception – commercial software which runs under PHP on IIS. I thought it would worth sharing a few gotchas in trying to pin down what was causing the problem:

1. Make sure you are looking for the

date.timezone

setting in the correct php.ini file (there are usually at least two) – you can identify which is being used by looking in IIS Manager | PHP Manager under ‘PHP Settings’ | ‘Configuration file’.

2. Use phpinfo to check what timezone PHP is reporting. You can either use the function –

phpinfo()

– in an otherwise empty php page or by choosing IIS Manager | PHP Manager and then under ‘PHP Setup’, choose ‘Check phpinfo()’.

3. Be aware that the windows PHP installer adds a section at the end of the php.ini file: 

[WebPIChanges]

. A date.timezone setting in this section will override any earlier date.timezone settings.

4. If you make any changes to php.ini, you will need to restart IIS in order for them to be picked up. At the command prompt, type

iisreset

or use IIS Manager.

5. If it’s your own code you can override the value in php.ini with:

date_default_timezone_set('Europe/London');

This is useful if you can’t restart IIS on e.g. a production server.

How to stop JQuery accordion scrolling to bottom of page

I struggled with this for a while until I found this great StackOverflow thread: http://stackoverflow.com/questions/3621161/jquery-accordion-will-it-scroll-to-the-top-of-the-open-item

What worked for me:

$("#accordion").accordion({autoHeight: false,active: 0});
$(".ui-accordion").bind("accordionchange", function(event, ui) {
     if ($(ui.newHeader).offset() != null) {
          ui.newHeader, // $ object, activated header
          $("html, body").animate({scrollTop: ($(ui.newHeader).offset().top)-100}, 500);
     }
});

There’s a lot of animation going on but it does exactly what is says on the tin.

Migrating Cordova/JQuery Mobile app from Android to iOS

Some notes on this process in case they are useful to others:

Back button

Android specifically prohibits back buttons due to the presence of a physical Back button on all Android mobiles. However, in iOS, it’s the norm. JQuery mobile does provide an easy to insert header back button :

<a href="#" data-role="button" data-rel="back" data-icon="arrow-l">Back</a>

but it uses the JQuery theme. If you want to make it look like an iOS back button, this article makes it relatively easy.

Navigation

In Android,with Cordova, we have to use absolute URLs for navigation:

navigator.app.loadUrl("file:///android_asset/www/mypage.html");

In iOS, we can use the much simpler form:

location.href = "mypage.html";

Seemingly random clicks firing intermittently

It seems that using JQuery Mobile’s ‘vclick’ event in iOS can cause problems, although the same code was working fine in Android. I was seeing a page change, followed by another page change as if something in the first page loaded had been clicked. After trying all sorts of event undelegation and unbinding, I finally looked at JQuery’s vclick documentation. This explains that there is a slight delay between the JQuery’s ‘vclick’ event  and the native ‘click’ event. When the content under the clicked point changes (as in this case with a new page load), the click event can actually fire on the changed rather than the original content. In situations like this, the recommendation is to use the ‘click’ rather than ‘vclick’ which solved the problem for me.

External pages

My app opens links to an external site for further information. In Android, simply specifying a full http:// prefix URL was enough to ensure that it loaded in the device’s normal web browser. In iOS, the default to seems to be to load it within the embedded brwoser which is being used to run your Cordova application, which is far from ideal. The workaround seems to simply add:

target = '_blank'

to all external links – they then fire up the iOS device’s Safari browser.

Using jsTree as a radio input

While adapting existing code to create a Basic LTI connector for Questionmark Perception, we needed some way of allowing administrators to choose which one of our many hundreds of assessments students using the same link should see. As the assessments are organised into folders, jsTree seemed the obvious way to present them.

jsTree does come with a plugin which allows users to tick check boxes next to each node, and there is even an option to allow only one chcek box to be selected at a time, reproducing radio button functionality. However, check boxes just don’t say ‘Pick one’ to me. Instead, we decided to use jsTree’s UI plugin and a hidden form field to deliver something that behaves as if there were a radio button next to each leaf node the tree.

The tree is created from nested unordered lists e.g:

<div id="tree_container">
    <ul>
        <li id="73053976"><a href="#">Folder 1</a>
            <ul>
                <li id="1223099925"><a href="#">Folder 1.1</a>
                    <ul>
                        <li id="7312166358986135"><a href="#">Assessment 1.1-1</a></li>
                        <li id="0272148079818008"><a href="#">Assessment 1.1-2</a></li>
                    </ul>
                </li>
                <li id="8430544147035550"><a href="#">Assessment 1-1</a></li>
                <li id="3489131659907643"><a href="#">Assessment 1-2</a></li>
            </ul>
        </li>
        <li id="759603613"><a href="#">Folder 2</a>
            <ul>
                <li id="8842542540288867"><a href="#">Assessment 2-1</a></li>
                <li id="5658598094790998"><a href="#">Assessment 2-2</a></li>
            </ul>
        </li>
    </ul>
</div>

Each list item has an id and those which contain other assessments (i.e. are folders) are given a class of folder.

There is also a form:

<form action="mypage.php" method="POST">
     <input id="selected_node_id" type="hidden" name="selected_node_id"></input> 
     <input type="submit" id="save_button" value="Save change" disabled="disabled" />
</form>

This contains a hidden input which will be used to post selected node id.

And a span to show the currently saved node_id:

<p>Currently selected:<strong><span id="selected_node_text"></span></strong></p>

The jsTree is then created, using the HTML_DATA plugin, as follows:

var dirty=false; //keep track of whether changes need to be saved
$(function () {
    $("#tree_container")
        .jstree({ 
            "plugins" : [ "themes", "html_data", "ui"],
              "themes" : {
                   "theme" : "classic",
                "dots" : true,
                "icons" : true
                  },
              "ui" : {
                  "select_limit" : 1  //only allow one node to be selected at a time
                  <?php if(isset($_SESSION['saved_node_id'])&&$_SESSION['saved_node_id']!='') echo ', "initially_select" : ['.$_SESSION['saved_node_id'].']'; ?>
                  }
            })
        .bind("select_node.jstree", function(event,data) {
            var this_node = data.rslt.obj;
            event.preventDefault(); //stop any link from being followed
            if(this_node.attr("class").indexOf('folder') != -1){
                $("#selected_node_id").val('');  //this is a folder so empty hidden input
                return data.inst.toggle_node(this_node);   //toogle closed/open state of this node to display/hide contents
                }
            else {
                $("#selected_node_id").val(this_node.attr("id")); //this is a leaf/assessment so set hidden input value to id
                if(this_node.attr("id")!=<?php echo $saved_node_id ?>){   //selected node is not the same as that previously saved so enable save and set dirty
                    document.getElementById('save_button').disabled = false;
                    dirty=true;
                    }
                else{ //selected node hasn't cnaged from that saved so diable save and unset dirty
                    document.getElementById('save_button').disabled = true; 
                    $("#selected_node_text").text(this_node.children("a").text()); //this is really only used to set selected_node_text when jsTree loads
                    dirty=false;
                    }
                }
            })
    });

Points to note:

  • "select_limit" : 1

    indicates that only one node may be selected at any one time;

  • the value of initially_select is set from the saved_node_id (which corresponds to a node/li id) if this is present in the session. This selects the previously selected node when the jsTree is loaded;
  • .bind("select_node.jstree", function(event,data) {

    deals with events when a node is selected and is also fired if initially_select is specified.

And finally, to ensure user doesn’t leave page before changes have been saved:

window.onbeforeunload = function(e) {
    if (dirty) return "You have not saved your choice.";
    }

Watch this space for how this is integrated into our new Basic LTI connector.

BasicLTI for QuestionMark Perception

Using Sakai to connect to load-balanced QuestionMark Perception using BasicLTI.

Why?

  1. Provide single sign-on capability to avoid maintaining a separate password for each user;
  2. Sakia (at Oxford) allows ‘external’ users to be created  from their email addresses. Where these users are given permission with Sakai and where they have accounts in Perception, they would then be able to access assessments.

How?

By adapting the QuestiionMark BasicLTI connector. This is a community edition project which the very helpful Steve Lay pointed us at: http://projects.oscelot.org/gf/project/lti-qm/frs/

Process as follows (PLEASE NOTE THAT I AM BLOGGING THIS AS I GO SO I APOLOGISE FOR ANY MISUNDERSTANDINGS – I’LL UPDATE AS MY LEVEL OF UNDERSTANDING INCREASES):

  1. Create DB – as we have a load-balanced setup and a clustered MSSQL DB, we had to create another db in our MSSQL server to avoid introducing a single point of failure. Create the tables using the following (adapted from http://www.spvsoftwareproducts.com/php/lti_tool_provider/)
    CREATE TABLE lti_consumer (
     consumer_key varchar(255) NOT NULL,
     name varchar(45) NOT NULL,
     secret varchar(32) NOT NULL,
     lti_version varchar(12) DEFAULT NULL,
     consumer_name varchar(255) DEFAULT NULL,
     consumer_version varchar(255) DEFAULT NULL,
     consumer_guid varchar(255) DEFAULT NULL,
     css_path varchar(255) DEFAULT NULL,
     protected tinyint NOT NULL,
     enabled tinyint NOT NULL,
     enable_from datetime DEFAULT NULL,
     enable_until datetime DEFAULT NULL,
     last_access date DEFAULT NULL,
     created datetime NOT NULL,
     updated datetime NOT NULL,
     PRIMARY KEY (consumer_key)
    );
    
    CREATE TABLE lti_context (
     consumer_key varchar(255) NOT NULL,
     context_id varchar(255) NOT NULL,
     lti_context_id varchar(255) DEFAULT NULL,
     lti_resource_id varchar(255) DEFAULT NULL,
     title varchar(255) NOT NULL,
     settings text,
     primary_consumer_key varchar(255) DEFAULT NULL,
     primary_context_id varchar(255) DEFAULT NULL,
     share_approved tinyint DEFAULT NULL,
     created datetime NOT NULL,
     updated datetime NOT NULL,
     PRIMARY KEY (consumer_key, context_id)
    );
    
    CREATE TABLE lti_user (
     consumer_key varchar(255) NOT NULL,
     context_id varchar(255) NOT NULL,
     user_id varchar(255) NOT NULL,
     lti_result_sourcedid varchar(255) NOT NULL,
     created datetime NOT NULL,
     updated datetime NOT NULL,
     PRIMARY KEY (consumer_key, context_id, user_id)
    );
    
    CREATE TABLE lti_nonce (
     consumer_key varchar(255) NOT NULL,
     value varchar(32) NOT NULL,
     expires datetime NOT NULL,
     PRIMARY KEY (consumer_key, value)
    );
    
    CREATE TABLE lti_share_key (
     share_key_id varchar(32) NOT NULL,
     primary_consumer_key varchar(255) NOT NULL,
     primary_context_id varchar(255) NOT NULL,
     auto_approve tinyint NOT NULL,
     expires datetime NOT NULL,
     PRIMARY KEY (share_key_id)
    );
    
    ALTER TABLE lti_context
     ADD CONSTRAINT lti_context_consumer_FK1 FOREIGN KEY (consumer_key)
     REFERENCES lti_consumer (consumer_key);
    
    ALTER TABLE lti_context
     ADD CONSTRAINT lti_context_context_FK1 FOREIGN KEY (primary_consumer_key, primary_context_id)
     REFERENCES lti_context (consumer_key, context_id);
    
    ALTER TABLE lti_user
     ADD CONSTRAINT lti_user_context_FK1 FOREIGN KEY (consumer_key, context_id)
     REFERENCES lti_context (consumer_key, context_id);
    
    ALTER TABLE lti_nonce
     ADD CONSTRAINT lti_nonce_consumer_FK1 FOREIGN KEY (consumer_key)
     REFERENCES lti_consumer (consumer_key);
    
    ALTER TABLE lti_share_key
     ADD CONSTRAINT lti_share_key_context_FK1 FOREIGN KEY (primary_consumer_key, primary_context_id)
     REFERENCES lti_context (consumer_key, context_id);
  2. Create a user on the DB whose details you will enter below.
  3. Create a new administrator in Enterprise Manager which will be used by QMWISE
  4. Create an MD5 checksum for the QMWISE user as described here: https://www.questionmark.com/perception/help/v5/product_guides/qmwise/Content/Testing/Calculating%20an%20MD5%20Checksum.htm
  5. Change settings in config.php:
    define('CONSUMER_KEY', 'testkey');define('CONSUMER_SECRET', 'testsecret');
    define('DB_NAME', 'sqlsrv:Server=localhost;Database=testdb');
    define('DB_USERNAME', 'dbusername');
    define('DB_PASSWORD', 'dbpassword');
    define('QMWISE_URL', 'http://localhost/QMWISe5/QMWISe.asmx');
    define('SECURITY_CLIENT_ID', 'ClientIDFromQMWISETestHarness');
    define('SECURITY_CHECKSUM', 'ChceksumFromQMWISETestHarness');
    define('DEBUG_MODE', true);
    define('ADMINISTRATOR_ROLE', 'LTI_INSTRUCTOR');
    define('QM_USERNAME_PREFIX', '');
    define('WEB_PATH', '/qmlti');

    DB_NAME should adhere to DSN conventions here: http://www.php.net/manual/en/ref.pdo-sqlsrv.connection.php

    SECURITY_CLIENT_ID is the administrator which is used by QMWISE

    SECURITY_CHECKSUM is the chceksu which can be generated from the QMWISE test harness

    WEB_PATH:  we needed to change to subdirectory where we had placed lti connector code

  6. As detailed here (https://www.questionmark.com/Developer/Pages/apis_documentation004.aspx) you cannot write applications yourself that provide a trust header so switch this off under Administration | Server Management | Server Settings in Enterprise Manager. Don’t forget to reset IIS (Start | All Programs | Accessories right-click Command Prompt and Run as Administrator then type
    iisreset

    ) to ensure it picks up the change in settings.

  7. Upload qmlti folder into C:\inetpub\wwwroot on both of the load-balanced servers
  8. Copy the lti.pip file into the \repository\PerceptionRepository\pip folder on the storage area shared by both load-balanced servers
  9. At this stage, I wanted to be able to launch an assessment as an IMS ‘Learner’/QM Participant from the test_harness. I couldn’t work out how to do this so made the following changes (these will probably be superseded once I have worked out how to set up the tool as an IMS ‘Instructor’ so that the correct assessment is shown based upon the User/Context_ID):
    • added a custom parameters textarea to the test_harness.php (188):
      <div class="row">
      <div class="col1">
      Custom Parameters (each on separate line; param_name=param_value)
      </div>
      <div class="col2">
      <textarea name="custom" rows="5" cols="50" onchange="onChange();" ><?php echo htmlentities($_SESSION['custom']); ?></textarea>
      </div>
      </div>

      ..and on line 45, something to save them:

      set_session('custom');
    • Something to read them and turn them into request parameters in test_launch.php (48):
      $custom_params = explode('\n', str_replace('\n\r','\n',$_SESSION['custom']));
      foreach($custom_params as $custom_param){
      if(strpos($custom_param, '=')){
      $custom_param_parts = explode('=', $custom_param);
      $params['custom_'.$custom_param_parts[0]]=$custom_param_parts[1];
      }
      }
    • And changed the parameter read in index.php (65) to look for the assessment ID with ‘custom_’ prepended as this seems to be what the specification expects:
      $assessment_id = $tool_provider->context->getSetting(custom_ASSESSMENT_SETTING);
  10. Don’t forget that you’ll need to ‘Allow run from integration’ under ‘Control settings’ for any assessment that you want to launch with QMWISE.
  11. Now pouint your browser to http://yourserver/qmlti/test_harness.php. It should pick up your key and secret automatically from config.php so, to launch an assessment as a student, all you need to do is:
    • enter the username of a valid participant in the User Details | ID box;
    • Enter the ID of an assessment for which that student is scheduled in the new custom parameters box as follows:
      ASSESSMENT_SETTING=1234567887654321
    • Click ‘Save data’ to..save your data
    • Then click Launch

It’s worth noting that doing things like this does breaks the strict separation between the various tiers as the PHP application is running on the QPLA servers and accessing the database directly.

Finally, a huge thanks to QuestionMark’s Steve Lay and to Stephen Vickers for his Basic LTI code.

 

Virtual microscopy in the classroom

This morning saw the first use of CSlide teaching slides within the laboratory to demonstrate the features that students should be looking for down their own microscopes. Monitors around the laboratory display the demonstrator’s computer screen or a view down the demonstrators microscope. The two main benefits of using CSlide on the computer, rather than projecting images from the demonstrator’s microscope, are:

  1. CSlide starts with a view of the whole slide and the demonstrator zooms in by double-clicking. This helps students get a feel for where they will find the relevant structures on their own slides. With a projected microscope, this overview is generally not possible and the change of lenses required for zooming in can make the experience disjointed.
  2. Each slide can be opened before the class, ready for use, in a separate window. This makes it very much quicker and easier to switch between slides, when teaching or responding to student questions, than with a traditional microscope.

The next stage is to embed the slides in students’ online teaching materials as has been done in the second year Nervous System course (Oxford users only). Annotations and ‘tours’ around the slides will give students a chance to come back to slides of interest outside the time-pressured environment of the laboratory. We also hope to build on these with self-test quizzes and decision maze case-studies.

 

Twitter Widget – filter by @username and #hashtag

If you want to embed tweets in your blog or website from a @username but only show those with a given #hashtag, Twitter’s Widgets makes it easy:

For example, to show only tweets from my @allotmentor account which are tagged with #allotment, I simply enter

from: @allotmentor AND #allotment

in the Search Query box on the Configure a search widget page. Then copy and paste the generated code into your site. See it in action here.

Thanks to this answer on Stack Overflow

Award hat trick

A duo of awards over the summer:

was topped off yesterday by news that we have been awarded FENS Funding for European Neuroscience History Projects in 2013 to capture and make available on our History of Medical Sciences website ‘3-dimensional images of physiological apparatus and models with historic interest’.
See all our summer news in the MSDLT Update