Friday 17 December 2010

Creating a Moodle course selector

Moodle has a user selector base class that is used in a few places to select users using a select box, for instance with two side-by-side user selectors to assign users to a role - one with users assigned the role, and one with users who are not assigned the role.

For a project I'm working on I need a similar set up, but for courses, to use in a block. Initially I added the code for the course selector with my block, but have since moved it into a separate local plugin, so it should be easier to reuse.

To start adapting the user selector I copied the /user/selector directory to /local/course_selector, and changed the relative paths inside the three files in that folder.

Also added a version.php and lang/en/local_course_selector.php to make it a full fledged local plugin.

Lots of simple changes

  1. First up in lib.php get rid of everything after line 646:
    // User selectors for managing group members ==================================
    I don't need this for my course selector, and it just complicates the string replacements below.

  2. Global string replacements:

    from to
    "user selector" "course selector"
    "user_selector" "course_selector"
    "M.core_user" "M.block_abc"
    "userselector" "courseselector"
    "users" "courses"

  3. change the two calls to moodle_exception to include the module name parameter
    so instead of
    throw new moodle_exception('cannotcallusgetselecteduser');
    it becomes
    throw new moodle_exception('cannotcallusgetselectedcourse', 'local_course_selector');
    so that the appropriate error message can be found in the language file for the block.

  4. Add these two lines to the language file local_course_selector.php:
    $string['courseselectortoomany'] = 'course_selector got more than one selected course, even though multiselect is false.';
    $string['cannotcallusgetselectedcourse'] = 'You cannot call course_selector::get_selected_course if multi select is true.';

  5. All get_string calls need to get 'local_course_selector' as a second parameter as well (some have a blank one '', some have none, some have 'moodle', it's probably best to replace them all with 'local_course_selector').

  6. Then add these to the language file as well:
    $string['clear'] = 'Clear';
    $string['searchoptions'] = 'Search options';
    $string['courseselectorpreserveselected'] = 'Keep selected courses, even if they no longer match the search';
    $string['courseselectorautoselectunique'] = 'If only one course matches the search, select it automatically';
    $string['courseselectorsearchanywhere'] = 'Match the search text anywhere in the course's name';
    $string['toomanycoursesmatchsearch'] = 'Too many courses ({$a->count}) match \'{$a->search}\'';
    $string['pleasesearchmore'] = 'Please search some more';
    $string['toomanycoursestoshow'] = 'Too many courses ({$a}) to show';
    $string['pleaseusesearch'] = 'Please use the search';
    $string['nomatchingcourses'] = 'No courses match \'{$a}\'';
    $string['none'] = 'None';
    $string['previouslyselectedcourses'] = 'Previously selected courses not matching \'{$a}\'';
    $string['search'] = 'Search';

  7. Next up I replaced all occurences of "user" with "course", unless it was $USER or had anything to do with user_preferences.

  8. change output_course function (renamed from output_user in the near-global replace) to convert a course object to a string.
    so that instead of "fullname($user)" it now does "$course->fullname"

Slightly more complex changes

Then it would seem we are nearly there. We have a course selector.

Except that it doesn't quite work, because the search_sql function creates SQL based on the user table, not the course table. This is mainly to make sure the references to firstname/lastname are replaced with fullname similar to how it was done for the output_course function above, and the $tests array contains the 'visible = 1' instead of the 'deleted = 0', etc.
'firstname', 'lastname' in the required_fields_sql function also need to be replaced with 'fullname'.


In module.js there are references to some strings as M.str.moodle.nomatchingusers and M.str.moodle.previouslyselectedusers. These are made available to JavaScript via hte $jsmodule definition in lib.php (approx. at line 67).

Two lines to change there into:

array('previouslyselectedcourses', 'local_course_selector', '%%SEARCHTERM%%'),
array('nomatchingcourses', 'local_course_selector', '%%SEARCHTERM%%'),

Now note that these will be available to the JavaScript code as M.str.local_course_selector.previouslyselectedcourses and M.str.local_course_selector.nomatchingcourses, so the references in module.js need to be updated accordingly.

Now all that's left is to realize that the css class has been renamed from userselector to courseselector, so any css styling will need to be applied to that. I haven't done this yet, so I'll "leave that as an exercise for the reader."


I think I've been lucky that the user selector wasn't more bound to the user object. It only keeps track of users using the ids, which made it very easy to do a global replace from user to course.

It was a fairly easy adaptation.

I only had a small issue with the Ajax version of the search only partially working, but once I'd figured out how the M.str.moodle strings turned up in the JavaScript and that I had to change the JavaScript reference to the scripts it actually seemed to work.

If one of the requirements for this project wasn't to not change any core Moodle code, but only add using any of the plugins available, I would probably have changed the existing user_selector so that it and this course_selector could share a common ancestor object_selector, which would do the bulk of the work, while the user_selector and course_selector would only deal with their specific needs.

Tuesday 7 December 2010

Developing on Windows, using Git, Dropbox and Ubuntu (and XAMPP)

I generally do my development work on Windows, mainly because a lot of it is .NET based. Occasionally I do non-.NET work (ruby/php), but since my main development pc is a Windows pc, I still usually use Windows.

Currently I have a client who uses Git for version control. Fair enough, I have TortoiseGit installed, so no problem, I thought. However we couldn't get the public/private key pair from my TortoiseGit/PuTTYgen to work with their Git server for authentication. I'm not sure why - something to do with the openssh keys being slightly different to the ones generated by PuTTYgen.

Since the client knows it works fine on Linux, it seemed to make sense to just go with Linux. But I still like to use my main development pc, which is a Windows pc.

Dropbox* to the rescue.

I have cloned the git repository into my Dropbox folder on my Linux pc. Dropbox syncs the files between my Linux and my Windows pc.

Now I can edit the files on my Windows pc and test them by running the Apache/MySQL/PHP based website on XAMPP. And while I'm doing the testing, Dropbox syncs my changes back to the cloned git repository on the Ubuntu pc.

It took a very long time to sync after the initial clone of the remote repository, and again after switching to the development branch, but after that it seems to work well.

TortoiseGit seems to be a bit confused with the synced git repository, so I'm not even going to try to actually use it for this project. Instead I'm using git on the command line on the Linux pc and that works fine.

A couple of points:

  • Sometimes there is a couple of seconds delay between saving files on Windows pc and appearing on the Linux pc, due to having to go through Dropbox, so it seems to be wise to wait a bit with git commands or at least check that the files have finished syncing.

  • Line endings. My editor saves with Windows style line endings by default, so I had to set it up to always save files with Unix style line endings (LF).

  • I have created symbolic links on Windows to link the git repository folder in the Dropbox folder into the htdocs folder in XAMPP (using mklink).
    mklink /D "moodle" "c:/users/me/my dropbox/git/moodle"
    The result is that I can now access the moodle site on http://localhost/moodle

  • I'm for now only working in one branch, and hope to keep it that way, as switching branches would possibly trigger another long sync time as it did the first time.

* If you don't have a Dropbox account, feel free to use this link to sign up and get a little bit of extra space. It will give me a bit of extra space as well.

Monday 6 December 2010

Motion Detecting Bird Feeder Cam

It has been snowing quite a bit recently so despite never having had much success with the birds, I decided to put our bird feeder out. It is hanging from a washing line, well away from any structures that could hide predators. Well, once the snow lady has melted it will be.

Reading and watching Oli Wood's bird feeder webcam inspired me to dig out our "old" digital video camera as well. Our camera is a Canon MVX350i, which has 20x optical zoom, just what's needed to be able to zoom in on the bird feeder at this distance.

The camera is connected to the computer using a Firewire cable, and once I had figured out how to switch off the "auto power save" feature we were good to go.

The camera is directed at the bird feeder using this expensive hand optimized stand:

Because we always only get very few birds on our feeder, I wanted to use motion detection webcam software to take pictures of any birds coming to the feeder. I vaguely remembered a friend using software that took pictures when it detected motion and uploaded them to an ftp server. I don't remember what it was called and hence had no luck finding it.

I did however find open source surveillance software iSpy that appears to do the trick.
Whenever it detects motion it records a bit of video up to a certain length. It has a few settings to change the motion detection and its sensitivity.

The first day I had it running it took a nice bit of video of what I believe is a robin, but completely missed the pair of tits that appeared on the feeder a few minutes later.

Increasing the sensitivity - I think I was increasing it: the slider doesn't mention which we is up or down, so I presume left is down, right is up - only seemed to get me lots of video of the wind blowing the feeder about, so in the end I stuck to the default sensitivity.

It would probably be better to make sure the bird feeder is rigid by sticking a pole in the ground and up its bottom, and increase the sensitivity. The pole should stop the wind blowing the feeder about, while the increase in sensitivity should pick up even the most delecate of birds.

One nice touch is that iSpy buffers the frames, so that when it detects motion it can use a bit of buffered video so you get all of the action, and not just the bird disappearing. It would otherwise possibly have missed this coal tit flying in:

The video of the robin is unedited. I trimmed about a minute of feeder waving about off the end of the coal tit one.