Dynamic cohorts

Dynamic cohort rule API

A dynamic cohort is a cohort whose members are determined based on a query. The query is composed of a number of "rules". The rules are grouped together into rulesets, stored in mdl_cohort_rulesets. Within each ruleset are multiple rules, stored in mdl_cohort_rules. Each rule, in turn has two main components:

  1. A "rule type". Much like the Moodle admin tree, or report builder column & filter options, rule types are defined via an array in a PHP file. The file is cohort/rules/settings.php, and the array is the

    $rules

    array returned by

    '''cohort_rules_list()'''
  2. Several "rule parameters", which are unique to that rule instance and stored in cohort_rule_params.

Examples

Rule: All users whose language type is not French or Spanish

  • rule type: User's language preference
  • rule parameters:
    • equal = 0
    • listofvalues={fr, sp}

Rule: All users who have completed the courses "Algebra I", "Trigonometery", and "Advanced Swimming" before October 31, 2011

  • rule type: Course completion date
  • rule parameters:
    • operator = 'before'
    • date = October 31, 2011
    • courses = {Algebra I, Trigonometry, Advanced Swimming}

Rule types

The rule types (and thus the rule menu) are defined in the

'''$rules'''

array in

'''cohort_rules_list()'''

in cohort/rules/settings.php. Each item in the array should be an instance of the

'''cohort_rule_option'''

class, taking four parameters:

  1. First two parameters: rules' group & name.
    • This combination should be unique, and it's used to identify the rules in the database.
    • Each time Totara needs to do anything with the rule, it will look up its group and name in the database, then pull the array from cohort_rule_option(), and get the right value from the array based on that group and name.
  2. It maps to*mdl_cohort_rules, the ruletype and name columns. (I should refactor ruletype to "rulegroup" or "grp" or something... unfortunately "group" is a SQL reserved word.).
    • Additionally, in the rule selector menu there's a heading for each group, with its name being

      get_string('rulegroup-'''\{$group\}'''', 'local_cohort')
    • Each rule's name should be in

      get_string('rulename-'''\{$group\}'''-'''\{$name\}'''', 'local_cohort')
  3. Rule's sqlhandler instance, which retrieves/stores the rule's parameters from the database, and generates the rule's SQL snippet
    • Each sqlhandler class represents one particular parameterized SQL snippet. For instance, the most basic, cohort_rule_sqlhandler_in_userfield_int, which handles rules which check whether an INTEGER column of the mdl_user table has a value in a particular list of values, i.e. "u.{$column} {$not} IN ({$listofvalues})"
  4. Rule's ui instance, which handles the dialog to create/edit the rule, and display a summary of it to the admin user
    • Each ui class represents one particular rule dialog type. The subclasses of cohort_rule_ui_form are particularly versatile, since most of them only display a dialog with a simple form

To add a new rule type to the menu:

  1. Identify the sqlhandler and ui you will need for your new rule type. 

    If no existing ones will work, then you'll need to implement new ones. See below.

  2. The sqlhandler and ui classes you use must have identical $params values.
  3. Add an entry to $rules
  4. If you've added a new group, define its name string, which will be

    get_string(''''rulegroup-\{$group\}'''', ''''local_cohort'''');
  5. . Define the rule type's name string, which will be

    get_string(''''rulename-\{$group\}-\{$name\}'''',''''local_cohort'''');

sqlhandlers

A little background, to explain what each sqlhandler does. The query that identifies the users in a dynamic cohort looks like this:

select u.*
from mdl_user u
where
	{SQL snippet for rule 1}
	AND {SQL snippet for rule 2}
	AND {SQL snippet for rule 3}
	...

In other words, each of the dynamic cohort's rules adds one condition to its WHERE clause. These are referred to here as "SQL snippets". Many of the rule types have very similar SQL snippets, for instance:

"u.lang \{$not\} in ($lov)"

,

"u.idnumber \{$not\} in (\{$lov\})"

,

"u.username \{$not\} in (\{$lov\})"

We've grouped those together to be handled by a single sqlhandler class, which takes parameters at its instantiation to distinguish them.

There's the potential for a little confusion here. sqlhandlers have two types of parameters:

  1. Parameters passed to the sqlhandler's constructor in settings.php 
    • These distinguish multiple*rule types that use the same sqlhandler class e.g. the "user's language preference", "user's idnumber", and "user's username" rule types all use

      cohort_rule_sqlhandler_in class

      Its constructor has the parameter "$column", with $column equal to 'lang', 'idnumber', or 'username'.

    • These are not configurable via the user interface; instead, the user chooses between them by their selection in the "add a rule" menu
  2. Parameters retrieved from the database table mdl_cohort_rule_params
    • These are the settings that the admin user has chosen, for this particular*instance of this rule type e.g. the rule "user's idnumber is 0001", "user's idnumber is 0002", etc. 
    • These are usually directly configurable via the user interface

In general, if something is configurable via the user interface when adding a new rule, it should go in mdl_cohort_rule_params. If something will be the same for all rules of that rule type, it should be a parameter to the sqlhandler's constructor (or it should just be a hard-coded part of the sqlhandler class).

Each sqlhandler has these vital components:

*$params

  • This is an array which defines what parameters must be stored in*mdl_cohort_rule_params for a rule instance which uses this sqlhandler. The keys of the array map to values in the name column. The values of the array are 0 or 1, to indicate whether the parameter is a scalar or an array.
    • Scalar values and array values are stored identically in mdl_cohort_rule_params; it's just that array values may have multiple rows (one for each entry in the array).
    • Values are all stored as char(255), so make sure you don't try to store anything too big, and also note that if you're storing foreign keys you may sometimes need to cast them to id using the following function in dmllib.php:

      sql_cast_to_int()
  • The*fetch() function will populate $this->{$name} variables for each of these params, in the sqlhandler instance. Array params will be an array, even if they have only one value. If they have no values, they'll be an empty array. Scalar values will be a scalar.
  • The*write() function will read $this->{$name} for each param and save it to the database.

    *fetch() and write() (you shouldn't need to overwrite these)

    • These are the functions that retrieve and write the parameters for a rule to the database.
    • As noted above, they populate/read the rule params to $this->{$name} variables in the sqlhandler class. fetch() also stores them in a $paramvalues array so that they can be easily accessible all in one place. This is how they're passed to the UI, for instance:

      *get_sql_snippet() (abstract)

    • Returns the SQL snippet for this rule. It assumes that you've already called fetch() for this sqlhandler instance. It takes the values that have been retrieved from the database, processes them (for instance converting stored numeric flags into SQL operators, and adding slashes to strings), and pops them into its parameterized SQL.
    • In the case where there is a family of queries that have similar preprocessing steps but different generated SQL, I've subclassed. The parent class's get_sql_snippet() function does all the preprocessing, then calls the child class's construct_sql_snippet() function.

The abstract base sqlhandler class below is defined in /cohort/rules/sqlhandler.php. Its subclasses are grouped together into files under /cohort/rules/sqlhandlers/. This isn't a proper extensible Moodle plugin type; to create new sqlhandlers you'll need to add them to an existing file, or create a new file and add a require_once() for it to /cohort/rules/settings.php

cohort_rule_sqlhandler

UIs

The cohort_rule_ui classes handle the UI for adding/editing/summarizing rules. They're pretty well described by the properties of the base class:

'''cohort_rule_ui'''

*$params

This is an array which identifies what parameters this UI will return from the user interface. It also indicates what parameters it expects to be fed if it will be describing an existing rule (whether to display its summary or to edit it).

  • This is the same format as cohort_rule_sqlhandler::$params, and a ui will only be compatible with a sqlhandler that has*exactly the same $params as it. 

    *$handlertype

  • This indicates which Javascript dialog handler this UI uses. (See the Totara Dialog documentation for more info on what this means). The Javascript for the handlers is in*cohort/rules/ruledialog.js.php.
    • The value here must have a matching version of the code below in the Javascript:

      totaraDialogs['cohortrule'+\{$handlertype\}]
  • Presently there are two handlers:*treeview and form

    *printDialogContent()

    • Prints the content of the "add rule"/"edit rule" dialog.

      *handleDialogUpdates($sqlhandler)

    • Called when a user hits the "save" button on the add/edit dialog. It should take the output from the dialog, feed it to the sqlhandler, and then tell the sqlhandler to save it. (There's some boilerplate code in this which could probably stand to be refactored out)

      *getRuleDescription()

    • This prints a one-line description of the rule. It's used on the "edit rules" page, to list all the existing rules for a cohort. It expects that all the values in $params have already been populated into the UI instance as $this->{$name} variables.

      *validateResponse()

    • To provide server-side validation of the form parameters entered by the user. It should return true if they're valid, false if otherwise.
      • If you're subclassing as below you don't need to use this function directly.

        cohort_rule_ui_form

        Instead, you should create a new 

        moodleform

        subclass, put your validation in there, and make your

        ui

        use that new form subclass. See below for an example.

        cohort_rule_ui_date
      • If you're not subclassing (as below) then you should also provide some code to print out an error message if validation fails.

        cohort_rule_ui_form

        Do this by storing your error strings into a class variable, then printing out that class variable in

        printDialogContent()

        *__construct(...)

    • There's no custom constructor class for the base cohort_rule_ui class, but any subclass which can be used in more than one rule type will need some constructor arguments to provide a different user interface for the different rule types. For instance, different labels for text fields & summaries.

In general, each ui class represents a particular set of input fields in a dialog. For instance, there is one ui class for a dialog with a text field and a "equal/not equal" menu; there is one for a date field and a menu indicating "before/after", etc.

cohort_ui_form

The most important subclass of

cohort_rule_ui

is as below, which handles dialogs that contain a standard Moodle formslib form.

'''cohort_rule_ui_form'''

You can subclass it by defining these special functions:

*addFormFields($mform)

    • This function should add elements to $mform, the same as you'd normally do in the definition() function of a Moodle form. You can also add client-side validation rules here (server-side validation rules will be ignored).

      *addFormData()

    • Returns an array that indicates the starting values of all the elements in the form. If you're editing an existing rule (i.e. all the $this-> variables are populated) then here you'll take the parameters for that rule and clean them up for the form. If you're adding a new rule, put your defaults in here. (For some reason using the formslib "setDefault()" function overwrites the $formdata, so put defaults here instead.)

      *$formclass

    • The name of the

      moodleform

      subclass to use. If you need to do server-side validation, you can write a new

      moodleform

      subclass with a

      validation()

      function you need, then use this variable to assign it to your UI class.

      *__construct(...)

    • If you're writing a ui class that can be used for more than one rule type, you'll need a custom constructor with some parameters to change the text so that users can tell what they're seeing. For instance, changing the strings used in the rule summarry, changing the labels of form fields...

treeview ui's

There are also three "treeview" subclasses (and their subsubclasses), which all handle multi-select treeviews. These are all quite similar, but not quite similar enough to have a common "treeview" subclass. In general, you can make a new treeview ui class by taking the PHP code out of the "find" file for an existing treeview dialog from elsewhere in Totara, and putting it in the

printDialogContent()

function of your ui class. Note that presently there's a hack in the multi-treeview dialogs which uses CSS to hide the "already selected" items from the dialog. I've counter-hacked my treeviews to override that CSS.

If you want to add form elements to the treeview (such as an "equal/notequal" menu), you'll want to subclass the

totara_dialog_content

subclass that you're using for your treeview, and override the

populate_selected_items_pane()

function to add your form elements at the top (or bottom?) of the selected items pane. There is some additional support for this in the handler:

  • Each form element that you want sent back should be given the class "cohorttreeviewsubmitfield".
  • If you want to perform client-side validation on the fields...
    • First surround them with a

      <div class="mform cohort-treeview-dialog-extrafields">

      and

      <fieldset>

      nested inside that; your validate form element goes inside the fieldset. (These tags make sure all the forms-related CSS matches up). One <fieldset> around each element or group of elements that should be highlighted in red if validation fails

    • *Don't put a

      <form>

      tag in there, though, because the treeview handler isn't able to properly capture form submits

    • In a

      <script>

      tag in the

      populate_selected_items_pane()

      function (actually, anywhere in

      printDialogContent()

      would probably work), add some Javascript that creates a new validation function and assigns it to the "

      cohort_validation_func

      " field of the element it should be validating. There is code in the cohortruletreeview handler which will call this function if the user clicks the "save" button on the dialog

    • You should also add this function to the

      onchange()

      handler for the element

    • The

      cohort_validation_func

      function should:

      • return true/false to indicate whether the element's current value is valid or not
      • if false, highlight the element by adding the "

        error

        " class to its surround fieldset. Also add an error message in a

        &lt;span class="error">

        tag inside the fieldset, to tell the user's what's wrong (if that error message isn't already there).

      • if true, remove the "

        error

        " class and the error message

    • See

      cohort_rule_ui_reportsto::getExtraSelectedItemsPaneWidgets()

      for an example of what this code should look like (eventually it would probably be good to have more code re-use on this, whether via a JS library function, or a PHP function that prints a bespoke JS function, or even a formslib form renderer...)

JS handlers

If you write a new JS handler type, take a look in ruledialog.js.php, at the function called

'''cohort_handler_responsefunc()'''

, and at the code right above that which binds to the "add rule" dropdown menus and the "edit rule" links to pop up the dialogs. Each time a dialog is called up, the handler for "add rule" or "edit rule" does the following:

  • In the case of a new rule, identifies the rule type that has been selected and whether the rule is going into a new ruleset or an existing ruleset. (These come from data attributes on the <option> tag that has been selected)
  • In the case of editing an existing rule, identifies the rule type and the rule's id (these come from the "edit" link)

    Construct's the dialog's "find" url, which will be a call to*ruledetail.php, which knows how to take the rule type and get the proper sqlhandler and ui class to get the contents of the dialog.

  • Constructs the dialog's "save" url, which will be the same as the "find" url but with "&update=1" (which is how ruledetail.php knows to process an update)

    Identifies the appropriate dialog & handler for this rule type (these come from a Javascript array printed in*rules.php based on the values in settings.php)

  • saves the variables "responsetype" and "responsegoeshere" to the handler, then opens the dialog.
    • responsetype is a string, which indicates what type of response to expect: add a new rule, add a new ruleset, or update an existing rule
    • responsegoeshere is a DOM object, which indicates where the response should go

When the dialog is saved, ruledetail.php provides a response which includes some JSON with new DOM objects to insert or update on the screen. The handler's save or submit function should process these in a standard way by calling *

cohort_handler_responsefunc()

*, which then reads the response and figures out what to do with it based on the values in "responsetype" and "responsegoeshere".