Commit fb44c5ab authored by Robert Lyon's avatar Robert Lyon
Browse files

Bug 1863511: Course completion (courseinfo) block



This block allows one to import information from an external source
It requires the connection via webservices to
1) find out the external user id value based on the page owner's email address
2) fetch the course completion data for that external user id

Change-Id: I6c67feb470bbcb35b0c549a8467c9204a00c9389
Signed-off-by: Robert Lyon's avatarRobert Lyon <robertl@catalyst.net.nz>
parent efee320b
<?php
/**
*
* @package mahara
* @subpackage admin
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('ADMIN', 1);
define('JSON', 1);
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
$plugintype = param_alpha('plugintype');
$pluginname = param_alpha('pluginname');
$type = param_alpha('type', null);
$result = array();
safe_require($plugintype, $pluginname);
if ($type) {
$classname = generate_artefact_class_name($type);
}
else {
$classname = generate_class_name($plugintype, $pluginname);
}
if (call_static_method($classname, 'has_config_info')) {
$info = call_static_method($classname, 'get_config_info');
$result['info_header'] = $info['header'];
$result['info_body'] = $info['body'];
json_reply(null, array('data' => $result));
}
else {
json_reply(true, array('message' => get_string('noinformation')));
}
......@@ -65,12 +65,19 @@ foreach (array_keys($plugins) as $plugin) {
foreach ($types as $t) {
$classname = generate_artefact_class_name($t);
if ($collapseto = call_static_method($classname, 'collapse_config')) {
$plugins[$plugin]['installed'][$key]['types'][$collapseto] = true;
$plugins[$plugin]['installed'][$key]['types'][$collapseto]['config'] = true;
}
else {
$plugins[$plugin]['installed'][$key]['types'][$t] =
$plugins[$plugin]['installed'][$key]['types'][$t]['config'] =
call_static_method($classname, 'has_config');
}
if ($collapseto = call_static_method($classname, 'collapse_config_info')) {
$plugins[$plugin]['installed'][$key]['types'][$collapseto]['info'] = true;
}
else {
$plugins[$plugin]['installed'][$key]['types'][$t]['info'] =
call_static_method($classname, 'has_config_info');
}
}
}
}
......@@ -80,6 +87,9 @@ foreach (array_keys($plugins) as $plugin) {
if (call_static_method($classname, 'has_config')) {
$plugins[$plugin]['installed'][$key]['config'] = true;
}
if (call_static_method($classname, 'has_config_info')) {
$plugins[$plugin]['installed'][$key]['info'] = true;
}
}
}
}
......
......@@ -988,6 +988,10 @@ abstract class ArtefactType implements IArtefactType {
return false;
}
public static function has_config_info() {
return false;
}
public static function get_config_options() {
return array();
}
......@@ -996,6 +1000,10 @@ abstract class ArtefactType implements IArtefactType {
return false;
}
public static function collapse_config_info() {
return false;
}
private function save_rolepermissions() {
if (!$this->group) {
return;
......
......@@ -432,3 +432,8 @@ $string['institutionunknown'] = '- unknown -';
$string['unabletodeleteadmin'] = 'Unable to delete user with ID "%s" as they are an admin';
$string['notuserblog'] = 'The journal is not owned by "%s"';
$string['oneof'] = 'One of';
$string['servicetype'] = 'Service type';
$string['authtype'] = 'Auth type';
$string['jsonenabled'] = 'JSON response';
$string['customfields'] = 'Custom fields';
<?php
/**
*
* @package mahara
* @subpackage blocktype-groupviews
* @author Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('JSON', 1);
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
require_once(get_config('docroot') . 'blocktype/lib.php');
require_once(get_config('docroot') . 'blocktype/courseinfo/lib.php');
$offset = param_integer('offset', 0);
$limit = param_integer('limit', 10);
if ($blockid = param_integer('block', null)) {
$bi = new BlockInstance($blockid);
$owner = $bi->get_view()->get('owner');
if ($owner) {
$options = $configdata = $bi->get('configdata');
$configdata['ownerid'] = $owner;
$courses = PluginBlocktypeCourseinfo::get_data($configdata, $offset, $limit);
$template = 'blocktype:courseinfo:courserows.tpl';
$baseurl = $bi->get_view()->get_url();
$baseurl .= ((false === strpos($baseurl, '?')) ? '?' : '&') . 'block=' . $blockid;
$pagination = array(
'baseurl' => $baseurl,
'id' => 'block' . $blockid . '_pagination',
'datatable' => 'coursedata_' . $blockid,
'jsonscript' => 'blocktype/courseinfo/courses.json.php',
);
PluginBlocktypeCourseinfo::render_courses($courses, $template, $options, $pagination);
json_reply(false, (object) array('message' => false, 'data' => $courses));
}
else {
json_reply(true, get_string('accessdenied', 'error'));
}
}
else {
json_reply(true, get_string('accessdenied', 'error'));
}
<?php
/**
*
* @package mahara
* @subpackage blocktype-courseinfo
* @author Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
defined('INTERNAL') || die();
$string['title'] = 'Course completion';
$string['name'] = 'Courseinfo';
$string['description'] = 'Display information about course completion from an external source';
$string['defaulttitledescription'] = 'A default title will be generated if you leave this blank.';
$string['blocktitleforowner'] = 'Course completion for %s';
$string['placeholdermessage'] = 'This block needs to be fully configured before it can be used. It can only be fully configured when it is on a user\'s portfolio page.';
$string['completeconfiguration'] = 'Complete configuring this block to activate it.';
$string['completeconfigurationnotpossible'] = 'The institution web service for this block needs to be established. Please ask you administrator to set it up.';
$string['unabletofetchdata'] = 'Unable to fetch data for this page owner';
$string['fromdate'] = 'From date';
$string['fromdatedescription'] = 'Only show courses started after this date. Use the format %s';
$string['todate'] = 'To date';
$string['todatedescription'] = 'Only show courses started before this date. Use the format %s';
$string['externaluserid'] = 'ID of external person';
$string['dateoutofsync'] = 'This needs to be older than the "To date".';
$string['nocourses'] = 'No course info to display.';
$string['hours'] = 'Hours';
$string['totalhours'] = 'Total hours';
$string['course'] = 'course';
$string['courses'] = 'courses';
$string['coursetype'] = 'Course type';
$string['connectedwithexternalaccount'] = 'External userid found';
$string['coursesresultsfromto'] = 'Courses found between %s and %s';
$string['coursesresultsfrom'] = 'Courses found from %s';
$string['coursesresultsto'] = 'Courses found to %s';
$string['completedondate'] = 'Completed on';
$string['organisation'] = 'Organisation';
$string['plugininfo'] = '<p>To display information about a person\'s course completion from an external site you need to set up the following:</p>
<ol>
<li>Have the \'blocktype/courseinfo\' plugin active</li>
<li>Have \'Allow outgoing web service requests\' turned on in "Administration" → "Webservices" → "Configuration" and have the \'Rest\' protocol active</li>
<li>Go to "Administration" → "Webservices" → "Connection manager" and choose the institution you want to make the connection for and then choose \'PluginBlocktypeCourseinfo:courseinfo - Course completion\' option in dropdown and then click \'Add\'</li>
<li>Fill in form with the following:
<ul>
<li>Name - give this instance a name, eg Institution A: Moodle</li>
<li>Connection enabled - set to \'Yes\'</li>
<li>Web service type - choose \'REST\'</li>
<li>Auth type - choose \'Token\'</li>
<li>Web service URL - set to the URL of the external source\'s REST server, eg http://moodle/webservice/rest/server.php</li>
<li>Token - set to the token generated on external source\'s side that has access to the exteral functions we require</li>
<li>Fixed parameters to pass - Add in any special parameters the URL needs to pass, eg for Moodle we need to add \'moodlewsrestformat=json\'</li>
<li>JSON encoded - set to \'Yes\'</li>
<li>External function for user ID - set this to the external service\'s function that can return a userid based on email address supplied, eg for Moodle \'core_user_get_users_by_field\'</li>
<li>External function for course completion - set this to the external service\'s function that can return course completion information based userid supplied, eg for Moodle \'local_wdhb_get_course_completion_data\'</li>
</ul>
<li>Save the form</li>
</ol>
<p>Now when a person adds the \'Course info\' block to their page it will fetch their external user ID and save it against the block and then when viewing the page it will fetch the completed courses of that user ID.</p>';
// For webservices
$string['novalidconnections'] = 'No valid connection objects.';
$string['novalidconnectionauthtype'] = 'No valid web service type. Needs to use the "REST" type.';
$string['connectionresultsinvalid'] = 'Unable to fetch results from external source.';
$string['userid_function_title'] = 'External function for user ID';
$string['coursecompletion_function_title'] = 'External function for course completion';
<?php
/**
*
* @package mahara
* @subpackage blocktype-courses
* @author Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
defined('INTERNAL') || die();
class PluginBlocktypeCourseinfo extends MaharaCoreBlocktype {
public static function get_title() {
return get_string('title', 'blocktype.courseinfo');
}
public static function get_instance_title(BlockInstance $instance) {
$ownerid = $instance->get_view()->get('owner');
if ($ownerid) {
return get_string('blocktitleforowner', 'blocktype.courseinfo', display_name($ownerid, null, true));
}
return get_string('title', 'blocktype.courseinfo');
}
public static function get_description() {
return get_string('description', 'blocktype.courseinfo');
}
public static function single_only() {
return true;
}
public static function get_categories() {
return array('general' => 16500);
}
public static function get_viewtypes() {
return array('portfolio');
}
public static function get_css_icon($blocktypename) {
return 'book-reader';
}
private static function check_connection_for_user($id) {
$user = new stdClass();
$user->id = $id;
if ($connections = Plugin::get_webservice_connections($user, 'fetch_userid')) {
return true;
}
return false;
}
public static function render_instance(BlockInstance $instance, $editing=false, $versioning=false) {
global $exporter;
$owner = $instance->get_view()->get('owner');
$configdata = $instance->get('configdata');
$smarty = smarty_core();
if (!array_key_exists('userid', $configdata) && !$owner) {
// We are just displaying the default message as the page is not owned by a user
$smarty->assign('message', get_string('placeholdermessage','blocktype.courseinfo'));
}
else if (!array_key_exists('userid', $configdata) && $owner) {
if ($editing) {
if (self::check_connection_for_user($owner)) {
// We display the complete configuration message if we are page owner
$smarty->assign('message', get_string('completeconfiguration', 'blocktype.courseinfo'));
}
else {
$smarty->assign('message', get_string('completeconfigurationnotpossible', 'blocktype.courseinfo'));
}
}
else {
$smarty->assign('message', get_string('nocourses','blocktype.courseinfo'));
}
}
else if (is_null($configdata['userid'])) {
// Unable to find external user related to page owner
if ($editing) {
if (self::check_connection_for_user($owner)) {
$smarty->assign('message', get_string('unabletofetchdata','blocktype.courseinfo'));
}
else {
$smarty->assign('message', get_string('completeconfigurationnotpossible','blocktype.courseinfo'));
}
}
else {
$smarty->assign('message', get_string('nocourses','blocktype.courseinfo'));
}
}
else {
$configdata['ownerid'] = $owner;
if ($exporter) {
$courses = self::get_data($configdata, 0, 0);
}
else {
$courses = self::get_data($configdata, 0, 10);
}
$template = 'blocktype:courseinfo:courserows.tpl';
$blockid = $instance->get('id');
if ($exporter) {
$pagination = false;
}
else {
$baseurl = $instance->get_view()->get_url();
$baseurl .= ((false === strpos($baseurl, '?')) ? '?' : '&') . 'block=' . $blockid;
$pagination = array(
'baseurl' => $baseurl,
'id' => 'block' . $blockid . '_pagination',
'datatable' => 'coursedata_' . $blockid,
'jsonscript' => 'blocktype/courseinfo/courses.json.php',
);
}
$configdata['block'] = $blockid;
self::render_courses($courses, $template, $configdata, $pagination, $editing, $versioning);
$smarty->assign('blockid', $instance->get('id'));
$from = !empty($configdata['from']) ? format_date($configdata['from'], 'strftimedate') : null;
$to = !empty($configdata['to']) ? format_date($configdata['to'], 'strftimedate') : null;
if ($from && $to) {
$resultstr = get_string('coursesresultsfromto', 'blocktype.courseinfo', $from, $to);
}
else if ($from && !$to) {
$resultstr = get_string('coursesresultsfrom', 'blocktype.courseinfo', $from);
}
else if (!$from && $to) {
$resultstr = get_string('coursesresultsto', 'blocktype.courseinfo', $to);
}
else {
$resultstr = '';
}
$smarty->assign('resultstr', $resultstr);
$smarty->assign('course', $courses);
}
return $smarty->fetch('blocktype:courseinfo:courseinfo.tpl');
}
public static function has_instance_config() {
return true;
}
public static function postinst($prevversion) {
if ($prevversion == 0) {
// do we need any post install stuff?
}
}
public static function instance_config_form(BlockInstance $instance) {
require_once('pieforms/pieform/elements/calendar.php');
$configdata = $instance->get('configdata');
$from = !empty($configdata['from']) ? $configdata['from'] : false;
$to = !empty($configdata['to']) ? $configdata['to'] : false;
$elements = array(
'from' => array(
'type' => 'calendar',
'title' => get_string('fromdate', 'blocktype.courseinfo'),
'description' => get_string('fromdatedescription', 'blocktype.courseinfo', pieform_element_calendar_human_readable_dateformat()),
'defaultvalue' => $from,
'caloptions' => array(
'showsTime' => false,
),
),
'to' => array(
'type' => 'calendar',
'title' => get_string('todate', 'blocktype.courseinfo'),
'description' => get_string('todatedescription', 'blocktype.courseinfo', pieform_element_calendar_human_readable_dateformat()),
'defaultvalue' => $to,
'caloptions' => array(
'showsTime' => false,
),
)
);
$ownerid = $instance->get_view()->get('owner');
if ($ownerid && !empty($configdata['userid'])) {
$elements['externaluser'] = array('type' => 'html',
'title' => get_string('externaluserid', 'blocktype.courseinfo'),
'value' => $configdata['userid'],
'class' => 'htmldescription');
}
return $elements;
}
public static function instance_config_validate(Pieform $form, $values) {
$viewid = $form->get_element_option('id', 'value');
$view = new View($viewid);
if (!empty($values['from']) && !empty($values['to']) && $values['from'] > $values['to']) {
$form->set_error('from', get_string('dateoutofsync', 'blocktype.courseinfo'));
}
}
public static function instance_config_save($values, BlockInstance $instance) {
$ownerid = $instance->get_view()->get('owner');
if ($ownerid) {
$username = get_field_sql("SELECT email FROM {artefact_internal_profile_email} WHERE owner = ? AND principal = ?", array($ownerid, 1)); // Check on primary email address
$owner = new stdClass();
$owner->id = $ownerid;
$configdata['username'] = $username;
if ($connections = Plugin::get_webservice_connections($owner, 'fetch_userid')) {
foreach ($connections as $connection) {
$result = call_static_method($connection->connection->class, 'fetch_userid', $connection, $owner, $configdata);
$values['userid'] = $result;
}
}
}
return $values;
}
public static function has_config_info() {
return true;
}
public static function get_config_info() {
return array('header' => self::get_title(),
'body' => get_string('plugininfo', 'blocktype.courseinfo'));
}
public static function default_copy_type() {
return 'shallow';
}
public static function get_data($configdata, $offset=0, $limit=10) {
$owner = new stdClass();
$owner->id = $configdata['ownerid'];
$content = false;
$total = 0;
if ($connections = Plugin::get_webservice_connections($owner, 'fetch_coursecompletion')) {
foreach ($connections as $connection) {
$content = call_static_method($connection->connection->class, 'fetch_coursecompletion', $connection, $owner, $configdata);
}
}
$results = array();
if ($content) {
$total = count($content['courses']);
foreach ($content['courses'] as $k => $course) {
$data = new StdClass();
$data->title = $course['name'];
$data->rawdate = $course['completiondate'];
$data->date = format_date($course['completiondate'], 'strftimedate');
$data->courselength = floatval($course['courselength']) > 0 ? $course['courselength'] : '-';
$data->hours = floatval($course['courselength']);
$data->category = $course['coursecategory'];
$data->cpdhours_display = floatval($course['cpdhours']) > 0 ? $course['cpdhours'] : '-';
$data->cpdhours = floatval($course['cpdhours']);
$data->type = $course['coursetype'];
$data->historical = $course['historical'];
$data->organisation = $course['courseorganisation'];
$data->id = $course['courseid'];
$data->uniqueid = $data->id . '_' . $data->rawdate;
$results[] = $data;
}
}
// Sort by latest course first
usort($results, function ($a, $b) {
if ($a->rawdate == $b->rawdate) { return 0; }
if ($a->rawdate < $b->rawdate) { return 1; }
if ($a->rawdate > $b->rawdate) { return -1; }
});
// calculate grand total of hours spent
$grandtotalhours = 0;
if (!empty($results)) {
foreach ($results as $result) {
$grandtotalhours = $grandtotalhours + $result->cpdhours;
}
}
if ($limit > 0) {
$results = array_slice($results, $offset, $limit);
}
$result = array(
'grandtotalhours' => $grandtotalhours,
'count' => $total,
'data' => $results,
'offset' => $offset,
'limit' => $limit,
'id' => 'course',
);
return $result;
}
public static function render_courses(&$courses, $template, $options, $pagination, $editing = false, $versioning = false) {
$smarty = smarty_core();
$smarty->assign('courses', $courses);
$smarty->assign('options', $options);
$smarty->assign('block', (!empty($options['block']) ? $options['block'] : null));
$smarty->assign('versioning', $versioning);
$courses['tablerows'] = $smarty->fetch($template);
if ($courses['limit'] && $pagination) {
$pagination = build_pagination(array(
'id' => $pagination['id'],
'class' => 'center',
'datatable' => $pagination['datatable'],
'url' => $pagination['baseurl'],
'jsonscript' => $pagination['jsonscript'],
'count' => $courses['count'],
'limit' => $courses['limit'],
'offset' => $courses['offset'],
'numbersincludefirstlast' => false,
'resultcounttextsingular' => get_string('course', 'blocktype.courseinfo'),
'resultcounttextplural' => get_string('courses', 'blocktype.courseinfo'),
));
$courses['pagination'] = $pagination['html'];
$courses['pagination_js'] = $pagination['javascript'];
}
}
public static function define_webservice_connections() {
return array(
array(
'connection' => 'courseinfo',
'name' => get_string('title', 'blocktype.courseinfo'),
'notes' => get_string('description', 'blocktype.courseinfo'),
'version' => '1.0',
'type' => WEBSERVICE_TYPE_REST,
'isfatal' => false,
'config_fields' => array(
'userid_function' => array(
'type' => 'text',
'title' => get_string('userid_function_title', 'blocktype.courseinfo'),
),
'coursecompletion_function' => array(
'type' => 'text',
'title' => get_string('coursecompletion_function_title', 'blocktype.courseinfo'),
),
),
),
);
}
public static function fetch_coursecompletion($connection, $user, $configdata) {
$data = array('userid' => $configdata['userid']);
if (!empty($configdata['from'])) {
$data['datefrom'] = $configdata['from'];
}
if (!empty($configdata['to'])) {
$data['dateto'] = $configdata['to'];
}
// check if we have any valid connection objects
if ($function = get_field('client_connections_config', 'value', 'field', 'coursecompletion_function', 'connection', $connection->connection->id)) {
$results = self::test_connection($connection, $user, $data, $function);
if ($results['error'] === false && !empty($results['results']) && is_array($results['results'])) {
return $results['results'];
}
}
return false;
}
public static function fetch_userid($connection, $user, $configdata) {
$data = array('field' => 'email',
'values' => array($configdata['username']));
// check if we have any valid connection objects
if ($function = get_field('client_connections_config', 'value', 'field', 'userid_function', 'connection', $connection->connection->id)) {
$results = self::test_connection($connection, $user, $data, $function);
if ($results['error'] === false && !empty($results['results']) && is_array($results['results'])) {