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.

No comments: