Changing site in WordPress Multisite (e.g. for getting posts from other sites)

I’ve been playing around with WordPress Multisite, and (in some custom plugin code) wanted to be able to get posts from and save posts to a different site to the one that I was in. Thankfully, there are a couple of functions that make this very easy:

switch_to_blog( $id )

restore_current_blog()

What these do is probably fairly self explanatory, but if you give switch_to_blog() the id of a blog, it will switch to that blog. Calling restore_current_blog() with then switch back to the original blog.

For example, to save a post to the current blog, and another blog, you can do the following:

//Post data
$newPost = array(
   'post_title' => 'title',
   'post_content' => 'content',
   'post_status' =>  'publish',
   'post_author' => $userId,
   'post_category' => 'category
);

$postID = wp_insert_post($newPost, false); //Insert post into current blog

switch_to_blog( $otherBlogID ); //Switch to the other blog

$otherPostID = wp_insert_post($newPost, false); //Insert post into other blog

restore_current_blog(); //Switch back to the current blog

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.

CSS hacks (underscore and star) for old IE versions

Ideally it shouldn’t be necessary to provide different CSS for different browsers, but sometimes needs must, particularly when it comes to dealing with the quirks of older versions of Internet Explorer (i.e. IE6 and IE7).

Thankfully there’s a couple of simple hacks that can be used to target CSS at old versions of IE only – the underscore (_) hack for IE6 (and below) and the star (*) hack for IE7 (and below). Just put _ or * in front of a CSS property as required, e.g.:

color: red;   /* all browsers */
*color: blue;   /* IE7 and below */
_color: green;   /* IE6 and below */

Using these rules, this sentence should appear green in IE6 and below, blue in IE7 and red in all other browsers.

There are other hacks that can be used. Check out these blog posts for more:

CSS Tip: Targeting IE 5.x, 6 and 7 Separately

IE7 only CSS hacks: Explained

Adding an image to a child theme template (e.g. header/footer) in WordPress

Child themes in WordPress are great, because they allow you to modify a theme without making changes to the theme’s core code, which makes things much easier when the theme gets updated – you don’t have to redo all of your changes.

I wanted to add an image to a custom footer template in a child theme (created by copying footer.php from the parent theme). I’d created an images directory in my child theme and put my image there, but was struggling to generate the correct link to the image.

After a bit of searching, I found this suggestion:

<img src="<?php echo get_template_directory_uri(); ?>/images/image.jpg" />

However, this was pointing to the image directory in the parent theme, not the child theme. Further searching led me to this blog: How to load files within WordPress themes, which had the answer (and explains things in more detail):

<img src="<?php echo get_stylesheet_directory_uri(); ?>/images/image.jpg" />

The basic difference, when using a child theme, is this:

  • Template refers to the parent theme
  • Stylesheet refers to the child theme

More information on the get_stylesheet_directory_uri() function can be found here: http://codex.wordpress.org/Function_Reference/get_stylesheet_directory_uri

Note: an alternative is to use the bloginfo() function (echo is not needed), as follows:

<img src="<?php bloginfo('stylesheet_directory'); ?>/images/image.jpg" />

However, it turns out that this is just an indirect way of calling get_stylesheet_directory_uri(), via get_bloginfo() in wp-includes/general-template.php. Both are included in the codex, so I don’t think it really matters which you use.

Another note: Dropping the _uri from the end of the function (i.e. get_stylesheet_directory()), will return the absolute server path to your child theme stylesheet directory, which can be used for including a another php file, e.g.

<?php include( get_stylesheet_directory() . '/includes/myfile.php'); ?>

More on this here: http://codex.wordpress.org/Function_Reference/get_stylesheet_directory

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.

 

jsTree: different contextmenu actions for different nodes

jsTree is a great jQuery plugin for easily creating expandable trees, e.g. from a list of items. We use it in our personalised portal page for Weblearn – see Personalising WebLearn (Sakai) – to display document trees.

We have been working on increasingly the functionality of this portal page to allow students to easily upload files. To do this, we are making use of the ‘contextmenu’ functionality of jsTree, which allows you to specify a menu of actions that can be accessed by right clicking on a tree node.

One thing that wasn’t immediately obvious from the jsTree docs was how to show different actions depending on the type of node, e.g. file or folder, on which the user clicked. However, it turns out that this can be done quite easily, by passing the ‘items’ parameter of the contextmenu configuration a function (that returns an object), rather than an object, as follows:

$('#tree_container').jstree({ 
    "plugins" : [ "contextmenu" ],
    "contextmenu" : {
        "items" : customMenu
    }
});

You then need to set up the customMenu function to return an items object, for example:

function customMenu(node) {
   //Show a different label for renaming files and folders
   if ($(node).hasClass("jstree-closed") || $(node).hasClass("jstree-open")) { //If node is a folder
      var renameLabel = "Rename Folder";
   }
   else {
      var renameLabel = "Rename File";
   }
   var items = {
      "upload" : {
          "label" : "Upload File",
          "action" : function () { ... }
      },
      "rename" : {
         "label" : renameLabel,   //Different label (defined above) will be shown depending on node type
         "action" : function () { ... }
      },
      "delete" : {
         "label" : "Delete File",
         "action" : function () { ... }
      }
   };

   //If node is a folder do not show the "delete" menu item
   if ($(node).hasClass("jstree-closed") || $(node).hasClass("jstree-open")) {
      delete items.remove;
   }

   return items;
}