Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Jocelyn Ireson-Paine -
Number of replies: 8

I've got to the point of trying to implement an unenrolment Web service, having discovered that the Moodle 2 Web services roadmap prescribes an enrol_manual_unenrol_users(), but that this isn't yet implemented. But I'm confused. Why does Moodle have so many plugins for enrolment and unenrolment, instead of one function for each that you can call with a parameter that tells it how to behave? These plugins are classes, I think, each with its own enrolment and unenrolment method. So to find an unenrolment method that I can call in my Web service, I have to read the class for each plugin, instead of just being able to call one function with a "behave this way" parameter. I feel like this:

Jocelyn

Average of ratings: -
In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Davo Smith -
Picture of Core developers Picture of Particularly helpful Moodlers Picture of Peer reviewers Picture of Plugin developers

Q. Why are there enrolment plugins?

A. Like so many aspects of Moodle, the plugins allow the functionality to be extended - if a particular organisation wants a new method of enroling/unenroling users from courses, then they can write one, instead of having to modify core code to get what they want. A single function call with a parameter would be fine, if you could somehow come up with a definitive list of every possible way that every possible organisation could possibly ever want to manage the enrolment / unenrolment of users on their courses.

Q. How can I find out which plugin should be doing the unenrolment?

A. If your web service is the main way of managing enrolment in your organisation's courses, then you should already know which enrolment method you are using (probably 'manual'). If, for some reason, you do not know what method a user is using and want to force them to be unenroled from the course regardless, then you can look up which method was used (and bear in mind, some methods, such as LDAP might automatically re-enrol them on the next updated, or a user might get upset if a course allowed PayPal enrolment, they've paid for the course and then the webservice has removed them).

To help you along the way of finding out which plugin has been used, look in the DB tables mdl_user_enrolments and mdl_enrol.

mdl_enrol will tell you the enrolment methods available for the course you are looking at (courseid), the name of the plugin (enrol) and the id of that instance of the enrolment plugin (id) (plus lots of other details). Looking in mdl_user_enrolments you can find a combination of the userid you are looking for and the enrolid you looked up in mdl_enrol. That will tell you what method was used to enrol the user, so you now know which method to call to unenrol the user. 

You may even find some helpful functions to do a lot of this for you, if you look in enrol/locallib.php.

Average of ratings: Useful (2)
In reply to Davo Smith

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Jocelyn Ireson-Paine -

Davo, thanks a lot for all the details. I talked to our administrator, and it seems that we do need manual unenrolment. However, I'm still confused. I keep chasing instance parameters around, without my chase ever bottoming out.

I started by looking at enrol/manual/lib.php , because I now know enough of Moodle conventions to know that important classes will probably be defined there. Lo and behold, I found "class enrol_manual_plugin extends enrol_plugin". So now, all I needed was its unenrolment method. Which, I hoped I could copy some unenrolment code from, or even call directly with a course ID and user ID as arguments.

But it doesn't have one. This was strange, because while poking around earlier, I'd found this code in enrol/locallib.php :

    /**
     * Unenrols a user from the course given the users ue entry
     *
     * @global moodle_database $DB
     * @param stdClass $ue
     * @return bool
     */
    public function unenrol_user($ue) {
        global $DB;
        list ($instance, $plugin) = $this->get_user_enrolment_components($ue);  (*)
        if ($instance && $plugin && $plugin->allow_unenrol($instance) && has_capability("enrol/$instance->enrol:unenrol", $this->context)) {
            $plugin->unenrol_user($instance, $ue->userid);
            return true;
        }
        return false;

When I first found it, I'd thought my search was over. I mean, look at the function name. But no, because like an NHS administrator, this function doesn't do any work. It just redelegates. In this case, it redelegates to this $ue thing. Which returns a list of enrolment components (*), one of which is a plugin. And the plugin's method unenrols the user.

So I look again at the manual plugin's method for unenrolling users. As mentioned above, it doesn't have one. Well, perhaps its superclass does. Let's look at class enrol_plugin.

A quick recursive grep showed me that class enrol_plugin lives in lib/enrollib.php . And yes, it has:

    public function unenrol_user(stdClass $instance, $userid) 

So $instance is the $plugin->unenrol_user referred to in enrol/locallib's unenrol_user. There doesn't seem to be an unenrolment function I can call directly, but perhaps I need to make an instance of manual_plugin and then call

  unenrol_user( that instance, my user's ID );

But then how do I know which parameter values to pass to the constructor?

Actually, I just stumbled across the method add_default_instance($course) in class enrol_manual_plugin. So perhaps if I construct an instance of enrol_manual_plugin (how?), then call add_default_instance(my course ID), and then call its unenrol_user(the same instance, my user's ID) ...?

But that seems horribly recursive. I'm missing some vital piece of documentation that tells me how these methods and instances fit together, and how to create the instances from scratch when I don't have them already set up by Moodle. Meanwhile, if my legs had moved as far as my fingers have this afternoon, I'd be halfway round the Oxford ring road and well on the way to finishing my customary two-hour run.

Jocelyn

In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Jocelyn Ireson-Paine -

Maybe I've made some progress. I realised that the built-in Web-service function for enrolling users probably needs to set up these instances in the same way that my unenrolment function would. So I grepped enrol_manual_enrol_users. It turns out to be mentioned in enrol/manual/db/services.php , which will tell me which class and method implements it.

And in enrol/manual/db/services.php , I found this:

$functions = array(
    ...
    'enrol_manual_enrol_users' => array(
        'classname'   => 'enrol_manual_external',
        'methodname'  => 'enrol_users',
        'classpath'   => 'enrol/manual/externallib.php',
        'description' => 'Manual enrol users',
        'capabilities'=> 'enrol/manual:enrol',
        'type'        => 'write',
    ),
);

So it's implemented by method enrol_users of class enrol_manual_external.

So I looked at enrol/manual/externallib.php . Where I found "class enrol_manual_external extends external_api", and in it, this method:

    /**
     * Enrolment of users
     * Function throw an exception at the first error encountered.
     * @param array $enrolments  An array of user enrolment
     * @return null
     */
    public static function enrol_users($enrolments) {
        global $DB, $CFG;

        require_once($CFG->libdir . '/enrollib.php');

        $params = self::validate_parameters(self::enrol_users_parameters(),
                array('enrolments' => $enrolments));

        $transaction = $DB->start_delegated_transaction(); //rollback all enrolment if an error occurs
                                                           //(except if the DB doesn't support it)

        //retrieve the manual enrolment plugin
        $enrol = enrol_get_plugin('manual');
        if (empty($enrol)) {
            throw new moodle_exception('manualpluginnotinstalled', 'enrol_manual');
        }

        foreach ($params['enrolments'] as $enrolment) {
            // Ensure the current user is allowed to run this function in the enrolment context
            $context = get_context_instance(CONTEXT_COURSE, $enrolment['courseid']);
            self::validate_context($context);

            //check that the user has the permission to manual enrol
            require_capability('enrol/manual:enrol', $context);

            //throw an exception if user is not able to assign the role
            $roles = get_assignable_roles($context);
            if (!key_exists($enrolment['roleid'], $roles)) {
                $errorparams = new stdClass();
                $errorparams->roleid = $enrolment['roleid'];
                $errorparams->courseid = $enrolment['courseid'];
                $errorparams->userid = $enrolment['userid'];
                throw new moodle_exception('wsusercannotassign', 'enrol_manual', '', $errorparams);
            }

            //check manual enrolment plugin instance is enabled/exist
            $enrolinstances = enrol_get_instances($enrolment['courseid'], true);
            foreach ($enrolinstances as $courseenrolinstance) {
              if ($courseenrolinstance->enrol == "manual") {
                  $instance = $courseenrolinstance;
                  break;
              }
            }
            if (empty($instance)) {
              $errorparams = new stdClass();
              $errorparams->courseid = $enrolment['courseid'];
              throw new moodle_exception('wsnoinstance', 'enrol_manual', $errorparams);
            }

            //check that the plugin accept enrolment (it should always the case, it's hard coded in the plugin)
            if (!$enrol->allow_enrol($instance)) {
                $errorparams = new stdClass();
                $errorparams->roleid = $enrolment['roleid'];
                $errorparams->courseid = $enrolment['courseid'];
                $errorparams->userid = $enrolment['userid'];
                throw new moodle_exception('wscannotenrol', 'enrol_manual', '', $errorparams);
            }

            //finally proceed the enrolment
            $enrolment['timestart'] = isset($enrolment['timestart']) ? $enrolment['timestart'] : 0;
            $enrolment['timeend'] = isset($enrolment['timeend']) ? $enrolment['timeend'] : 0;
            $enrolment['status'] = (isset($enrolment['suspend']) && !empty($enrolment['suspend'])) ?
                    ENROL_USER_SUSPENDED : ENROL_USER_ACTIVE;

            $enrol->enrol_user($instance, $enrolment['userid'], $enrolment['roleid'],
                    $enrolment['timestart'], $enrolment['timeend'], $enrolment['status']);

        }

        $transaction->allow_commit();
    }

So I suspect that if I put in some "un"s, this might work:

    /**
     * Unenrolment of users
     * Function throw an exception at the first error encountered.
     * @param array $unenrolments  An array of user unenrolments.
     * @return null
     */
    public static function unenrol_users( $unenrolments ) {
        global $DB, $CFG;

        require_once($CFG->libdir . '/enrollib.php');

        $params = self::validate_parameters(self::unenrol_users_parameters(),
                array( 'unenrolments' => $unenrolments) );

        $transaction = $DB->start_delegated_transaction(); //rollback all unenrolment if an error occurs
                                                           //(except if the DB doesn't support it)

        //retrieve the manual enrolment plugin
        $enrol = enrol_get_plugin('manual');
        if (empty($enrol)) {
            throw new moodle_exception('manualpluginnotinstalled', 'enrol_manual');
        }

        foreach ($params['unenrolments'] as $unenrolment) {
            // Ensure the current user is allowed to run this function in the enrolment context
            $context = get_context_instance(CONTEXT_COURSE, $enrolment['courseid']);
            self::validate_context($context);

            //check that the user has the permission to manual unenrol
            require_capability('enrol/manual:unenrol', $context);
            
            // Should we unassign roles here?

            //check manual enrolment plugin instance is enabled/exist
            $enrolinstances = enrol_get_instances($enrolment['courseid'], true);
            foreach ($enrolinstances as $courseenrolinstance) {
              if ($courseenrolinstance->enrol == "manual") {
                  $instance = $courseenrolinstance;
                  break;
              }
            }
            if (empty($instance)) {
              $errorparams = new stdClass();
              $errorparams->courseid = $enrolment['courseid'];
              throw new moodle_exception('wsnoinstance', 'enrol_manual', $errorparams);
            }

            //check that the plugin accepts unenrolment (it should always the case, it's hard coded in the plugin)
            if (!$enrol->allow_unenrol($instance)) {
                $errorparams = new stdClass();
                $errorparams->roleid = $enrolment['roleid'];
                $errorparams->courseid = $enrolment['courseid'];
                $errorparams->userid = $enrolment['userid'];
                throw new moodle_exception('wscannotenrol', 'enrol_manual', '', $errorparams);
            }
            // Probably we need the role ID. Can one unenrol a user
            // in one role but not in another?

            //finally proceed the enrolment
            $enrolment['timestart'] = isset($enrolment['timestart']) ? $enrolment['timestart'] : 0;
            $enrolment['timeend'] = isset($enrolment['timeend']) ? $enrolment['timeend'] : 0;
            $enrolment['status'] = (isset($enrolment['suspend']) && !empty($enrolment['suspend'])) ?
                    ENROL_USER_SUSPENDED : ENROL_USER_ACTIVE;
            // Don't know whether we want similar assignments.

            $enrol->unenrol_user($instance, $enrolment['userid'], $enrolment['roleid'],
                    $enrolment['timestart'], $enrolment['timeend'], $enrolment['status']);
            // Don't know whether we want similar parameters.

        }

        $transaction->allow_commit();
    }

Moodle developers, any comments?
Jocelyn

In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Jocelyn Ireson-Paine -

I made some more progress. In webservice\simpletest\testwebservice.php , there's a set of Web-service tests, one for each Web-service function. I realised that, for tests of enrol_manual_enrol_users and core_enrol_get_enrolled_users, I might find code that unenrols users. Either to set up the environment for the tests, or to clean up after them.

And I did. In moodle_group_create_groups, there's this code:

        //unenrol the user
        $DB->delete_records('user_enrolments', array('id' => $enrolment->id));
        $DB->delete_records('enrol', array('id' => $enrol->id));
        role_unassign($role1->id, $user->id, $context->id);

And in moodle_enrol_manual_enrol_users, there's this:

        $enrolments = array();
        $courses = $DB->get_records('course');

        foreach ($courses as $course) {
            if ($course->id > 1) {
                $enrolments[] = array('roleid' => $roleid,
                    'userid' => $user->id, 'courseid' => $course->id);
                $enrolledcourses[] = $course;
            }
        }

        //web service call
        $function = 'moodle_enrol_manual_enrol_users';
        $wsparams = array('enrolments' => $enrolments);
        $enrolmentsresult = $client->call($function, $wsparams);

        //get instance that can unenrol
        $enrols = enrol_get_plugins(true);
        $enrolinstances = enrol_get_instances($course->id, true);
        $unenrolled = false;
        foreach ($enrolinstances as $instance) {
            if (!$unenrolled and $enrols[$instance->enrol]->allow_unenrol($instance)) {
                $unenrolinstance = $instance;
                $unenrolled = true;
            }
        }

        //test and unenrol the user
        $enrolledusercourses = enrol_get_users_courses($user->id);
        foreach ($enrolledcourses as $course) {
            //test
            $this->assertEqual(true, isset($enrolledusercourses[$course->id]));

            //unenrol the user
            $enrols[$unenrolinstance->enrol]->unenrol_user($unenrolinstance, $user->id, $roleid);
        }

I'd already realised that there would be different levels of abstraction at which I could implement unenrol. At the lowest, I could work directly on the database. That's what the first piece of code does. This has the disadvantage that if the database layout is changed in a future version, my code might no longer work. I did find a document describing the database schema at Database schema introduction . But it doesn't say whether future versions are guaranteed to retain this schema, and it doesn't say much about assumptions: about relationships between tables that need to be preserved whenever anyone updates the database.

That sort of info really is important for people like me who come to the source code fresh, because without it, I might violate some condition that a developer assumed would always hold, and break his code. 

Also, I couldn't see much about enrolment. There was a section that looked promising. But this is what it said:

  Enrolment plugins

  • ...

The other piece of code works at a higher level of abstraction, on plugins. It's useful because it avoids low-level details that might change. It also tells me something about how all these instances that the plugin system passes around are related. I still don't understand that, but by a combination of intuition and mindless program transformation (deleting variables on which those below don't depend), I made this:

 
function unenrol( $user_id, $course_id, $role_id )
{
  $enrolinstances = enrol_get_instances( $course_id, true );

  echo 'Got $enrolinstances: '; print_r( $enrolinstances ); echo ".\n";

  $enrols = enrol_get_plugins( true );

  echo 'Got $enrols: '; print_r( $enrols ); echo ".\n";

  $unenrolled = false;
  foreach ( $enrolinstances as $instance ) {
    if ( !$unenrolled and $enrols[ $instance->enrol ]->allow_unenrol( $instance ) ) {
      $unenrolinstance = $instance;
      $unenrolled = true;
    }
  }
  // This looks as though it selects only those instances that
  // can unenrol. By dumping the instances, I discovered that
  // they have an 'enrol' field which indicates the kind of
  // plugin. For manual enrolment, the field contains 'manual'.
  // And for the course I tried, $enrolinstances didn't contain
  // any others. I don't know whether that would always be so.

  echo 'Got $unenrolinstance: '; print_r( $unenrolinstance ); echo ".\n";

  // Should check that $unenrolinstance->enrol is 'manual'. If it
  // is, the statement below is probably safe.

  $enrols[ $unenrolinstance->enrol ]->unenrol_user( $unenrolinstance, $user_id, $role_id );
}

Now, I have to test it.

Jocelyn

In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Jocelyn Ireson-Paine -

Well, the function I showed at the end of the previous post seems to work, so I've installed it as a Web service. I'd really appreciate it if one of the programmers who coded these plugins could check the code, in case it depends on assumptions that I don't know about. For example, might $unenrolinstances sometimes not contain a manual enrolment plugin? 

If it's safe, Moodle 2.3 could use it to implement enrol_manual_unenrol_users.

Jocelyn

In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Chris Megahan -
Picture of Core developers

Have you found anything further since this posting? I'm currently in the process of implementing connecting moodle web services to our SIS and the lack of a unenrol functionality in web services will be an issues for us.

In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Chris Megahan -
Picture of Core developers

Have you found anything further since this posting? I'm currently in the process of implementing connecting moodle web services to our SIS and the lack of a unenrol functionality in web services will be an issues for us.

In reply to Jocelyn Ireson-Paine

Re: Wny are there enrolment plugins instead of a function for enrolment and a function for unenrolment?

by Sean Neilan -

I can't believe no moodle core developers have responded to this post. This post perfectly captures the experience that is developing for moodle. 

Can a moodle developer elaborate on some of these experiences? I understand that they know the system well but people who are new-comers don't know what you know.