diff --git a/admin/tool/cohortroles/classes/api.php b/admin/tool/cohortroles/classes/api.php
new file mode 100644
index 00000000000..36b0d1dbf62
--- /dev/null
+++ b/admin/tool/cohortroles/classes/api.php
@@ -0,0 +1,229 @@
+.
+
+/**
+ * Class exposing the api for the cohortroles tool.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_cohortroles;
+
+use stdClass;
+use context_system;
+use tool_lp\invalid_persistent_exception;
+
+/**
+ * Class for doing things with cohort roles.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class api {
+
+ /**
+ * Create a cohort role assignment from a record containing all the data for the class.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @param stdClass $record Record containing all the data for an instance of the class.
+ * @return competency
+ */
+ public static function create_cohort_role_assignment(stdClass $record) {
+ $cohortroleassignment = new cohort_role_assignment(0, $record);
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // Validate before we check for existing records.
+ if (!$cohortroleassignment->is_valid()) {
+ throw new invalid_persistent_exception($cohortroleassignment->get_errors());
+ }
+
+ $existing = cohort_role_assignment::get_record((array) $record);
+ if (!empty($existing)) {
+ return false;
+ } else {
+ // OK - all set.
+ $cohortroleassignment->create();
+ }
+ return $cohortroleassignment;
+ }
+
+ /**
+ * Delete a cohort role assignment by id.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @param int $id The record to delete. This will also remove this role from the user for all users in the system.
+ * @return boolean
+ */
+ public static function delete_cohort_role_assignment($id) {
+ $cohortroleassignment = new cohort_role_assignment($id);
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // OK - all set.
+ return $cohortroleassignment->delete();
+ }
+
+ /**
+ * Perform a search based on the provided filters and return a paginated list of records.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @param string $sort The column to sort on
+ * @param string $order ('ASC' or 'DESC')
+ * @param int $skip Number of records to skip (pagination)
+ * @param int $limit Max of records to return (pagination)
+ * @return array of cohort_role_assignment
+ */
+ public static function list_cohort_role_assignments($sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // OK - all set.
+ return cohort_role_assignment::get_records(array(), $sort, $order, $skip, $limit);
+ }
+
+ /**
+ * Perform a search based on the provided filters and return a paginated list of records.
+ *
+ * Requires moodle/role:manage capability at system context.
+ *
+ * @param array $filters A list of filters to apply to the list.
+ * @return int
+ */
+ public static function count_cohort_role_assignments() {
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // OK - all set.
+ return cohort_role_assignment::count_records();
+ }
+
+ /**
+ * Sync all roles - adding and deleting role assignments as required.
+ *
+ * Slow. Should only be called from a background task.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @return array('rolesadded' => array of (useridassignedto, useridassignedover, roleid),
+ * 'rolesremoved' => array of (useridassignedto, useridassignedover, roleid))
+ */
+ public static function sync_all_cohort_roles() {
+ global $DB;
+
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // Ok ready to go.
+ $rolesadded = array();
+ $rolesremoved = array();
+
+ // Get all cohort role assignments and group them by user and role.
+ $all = cohort_role_assignment::get_records(array(), 'userid, roleid');
+ // We build an better structure to loop on.
+ $info = array();
+ foreach ($all as $cra) {
+ if (!isset($info[$cra->get_userid()])) {
+ $info[$cra->get_userid()] = array();
+ }
+ if (!isset($info[$cra->get_userid()][$cra->get_roleid()])) {
+ $info[$cra->get_userid()][$cra->get_roleid()] = array();
+ }
+ array_push($info[$cra->get_userid()][$cra->get_roleid()], $cra->get_cohortid());
+ }
+ // Then for each user+role combo - find user context in the cohort without a role assigned.
+
+ foreach ($info as $userid => $roles) {
+ foreach ($roles as $roleid => $cohorts) {
+ list($cohortsql, $params) = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED);
+
+ $params['usercontext'] = CONTEXT_USER;
+ $params['roleid'] = $roleid;
+ $params['userid'] = $userid;
+
+ $sql = 'SELECT u.id AS userid, ra.id, ctx.id AS contextid
+ FROM {user} u
+ JOIN {cohort_members} cm ON u.id = cm.userid
+ JOIN {context} ctx ON u.id = ctx.instanceid AND ctx.contextlevel = :usercontext
+ LEFT JOIN {role_assignments} ra ON ra.contextid = ctx.id
+ AND ra.roleid = :roleid
+ AND ra.userid = :userid
+ WHERE cm.cohortid ' . $cohortsql . '
+ AND ra.id IS NULL';
+
+ $toadd = $DB->get_records_sql($sql, $params);
+
+ foreach ($toadd as $add) {
+ role_assign($roleid, $userid, $add->contextid, 'tool_cohortroles');
+ $rolesadded[] = array(
+ 'useridassignedto' => $userid,
+ 'useridassignedover' => $add->userid,
+ 'roleid' => $roleid
+ );
+ }
+ }
+ }
+
+ // And for each user+role combo - find user context not in the cohort with a role assigned.
+ // If the role was assigned by this component, unassign the role.
+ foreach ($info as $userid => $roles) {
+ foreach ($roles as $roleid => $cohorts) {
+ // Now we are looking for entries NOT in the cohort.
+ list($cohortsql, $params) = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED);
+
+ $params['usercontext'] = CONTEXT_USER;
+ $params['roleid'] = $roleid;
+ $params['userid'] = $userid;
+ $params['component'] = 'tool_cohortroles';
+
+ $sql = 'SELECT u.id as userid, ra.id, ctx.id AS contextid
+ FROM {user} u
+ JOIN {context} ctx ON u.id = ctx.instanceid AND ctx.contextlevel = :usercontext
+ JOIN {role_assignments} ra ON ra.contextid = ctx.id AND ra.roleid = :roleid AND ra.userid = :userid
+ LEFT JOIN {cohort_members} cm ON u.id = cm.userid
+ AND cm.cohortid ' . $cohortsql . '
+ WHERE ra.component = :component AND cm.cohortid IS NULL';
+
+ $toremove = $DB->get_records_sql($sql, $params);
+ foreach ($toremove as $remove) {
+ role_unassign($roleid, $userid, $remove->contextid, 'tool_cohortroles');
+ $rolesremoved[] = array(
+ 'useridassignedto' => $userid,
+ 'useridassignedover' => $remove->userid,
+ 'roleid' => $roleid
+ );
+ }
+ }
+ }
+
+ return array('rolesadded' => $rolesadded, 'rolesremoved' => $rolesremoved);
+ }
+
+}
diff --git a/admin/tool/cohortroles/classes/cohort_role_assignment.php b/admin/tool/cohortroles/classes/cohort_role_assignment.php
new file mode 100644
index 00000000000..6c1e4a10973
--- /dev/null
+++ b/admin/tool/cohortroles/classes/cohort_role_assignment.php
@@ -0,0 +1,107 @@
+.
+
+/**
+ * Class for cohort_role_assignment persistence.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_cohortroles;
+
+use lang_string;
+use tool_lp\persistent;
+
+/**
+ * Class for loading/storing cohort_role_assignments from the DB.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_role_assignment extends persistent {
+
+ /** Table name for user_competency persistency */
+ const TABLE = 'tool_cohortroles';
+
+ /**
+ * Return the definition of the properties of this model.
+ *
+ * @return array
+ */
+ protected static function define_properties() {
+ return array(
+ 'userid' => array(
+ 'type' => PARAM_INT,
+ ),
+ 'roleid' => array(
+ 'type' => PARAM_INT,
+ ),
+ 'cohortid' => array(
+ 'type' => PARAM_INT,
+ )
+ );
+ }
+
+ /**
+ * Validate the user ID.
+ *
+ * @param int $value The value.
+ * @return true|lang_string
+ */
+ protected function validate_userid($value) {
+ global $DB;
+
+ if (!$DB->record_exists('user', array('id' => $value))) {
+ return new lang_string('invaliduserid', 'error');
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate the role ID.
+ *
+ * @param int $value The value.
+ * @return true|lang_string
+ */
+ protected function validate_roleid($value) {
+ global $DB;
+
+ if (!$DB->record_exists('role', array('id' => $value))) {
+ return new lang_string('invalidroleid', 'error');
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate the cohort ID.
+ *
+ * @param int $value The value.
+ * @return true|lang_string
+ */
+ protected function validate_cohortid($value) {
+ global $DB;
+
+ if (!$DB->record_exists('cohort', array('id' => $value))) {
+ return new lang_string('invalidcohortid', 'error');
+ }
+
+ return true;
+ }
+
+}
diff --git a/admin/tool/cohortroles/classes/form/assign_role_cohort.php b/admin/tool/cohortroles/classes/form/assign_role_cohort.php
new file mode 100644
index 00000000000..9292b9beff1
--- /dev/null
+++ b/admin/tool/cohortroles/classes/form/assign_role_cohort.php
@@ -0,0 +1,82 @@
+.
+
+/**
+ * Assign role to cohort form.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\form;
+defined('MOODLE_INTERNAL') || die();
+
+use moodleform;
+use context_system;
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Assign role to cohort form.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assign_role_cohort extends moodleform {
+
+ public function definition() {
+ global $OUTPUT;
+
+ $mform = $this->_form;
+ $roles = get_roles_for_contextlevels(CONTEXT_USER);
+
+ if (empty($roles)) {
+ $warning = $OUTPUT->notify_problem(get_string('noassignableroles', 'tool_cohortroles'));
+ $mform->addElement('html', $warning);
+ return;
+ }
+
+ $options = array(
+ 'ajax' => 'tool_lp/form-user-selector',
+ 'multiple' => true
+ );
+ $mform->addElement('autocomplete', 'userids', get_string('selectusers', 'tool_cohortroles'), array(), $options);
+ $mform->addRule('userids', null, 'required');
+
+ $names = role_get_names();
+ $options = array();
+ foreach ($roles as $idx => $roleid) {
+ $options[$roleid] = $names[$roleid]->localname;
+ }
+
+ $mform->addElement('select', 'roleid', get_string('selectrole', 'tool_cohortroles'), $options);
+ $mform->addRule('roleid', null, 'required');
+
+ $context = context_system::instance();
+ $options = array(
+ 'ajax' => 'tool_lp/form-cohort-selector',
+ 'multiple' => true,
+ 'data-contextid' => $context->id,
+ 'data-includes' => 'all'
+ );
+ $mform->addElement('autocomplete', 'cohortids', get_string('selectcohorts', 'tool_cohortroles'), array(), $options);
+ $mform->addRule('cohortids', null, 'required');
+ $mform->addElement('submit', 'submit', get_string('assign', 'tool_cohortroles'));
+ }
+
+}
diff --git a/admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php b/admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php
new file mode 100644
index 00000000000..7d1e72030cc
--- /dev/null
+++ b/admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php
@@ -0,0 +1,213 @@
+.
+
+/**
+ * Cohort role assignments table
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\output;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+use context_helper;
+use context_system;
+use html_writer;
+use moodle_url;
+use table_sql;
+
+/**
+ * Cohort role assignments table.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_role_assignments_table extends table_sql {
+
+ /**
+ * Sets up the table.
+ *
+ * @param string $uniqueid Unique id of table.
+ * @param \tool_lp\template $template The template.
+ */
+ public function __construct($uniqueid, $url) {
+ global $CFG;
+ parent::__construct($uniqueid);
+ $context = context_system::instance();
+
+ $this->context = $context;
+
+ $this->rolenames = role_get_names();
+
+ // This object should not be used without the right permissions.
+ require_capability('moodle/role:manage', $context);
+
+ $this->useridfield = 'userid';
+
+ // Define columns in the table.
+ $this->define_table_columns();
+
+ $this->define_baseurl($url);
+ // Define configs.
+ $this->define_table_configs();
+ }
+
+ protected function col_rolename($data) {
+ return $this->rolenames[$data->roleid]->localname;
+ }
+
+ protected function col_cohortname($data) {
+ global $OUTPUT;
+
+ $record = (object) array(
+ 'id' => $data->cohortid,
+ 'idnumber' => $data->cohortidnumber,
+ 'description' => $data->cohortdescription,
+ 'visible' => $data->cohortvisible,
+ 'name' => $data->cohortname
+ );
+ $context = context_helper::instance_by_id($data->cohortcontextid);
+
+ $exporter = new \tool_lp\external\cohort_summary_exporter($record, array('context' => $context));
+ $cohort = $exporter->export($OUTPUT);
+
+ $html = $OUTPUT->render_from_template('tool_cohortroles/cohort-in-list', $cohort);
+ return $html;
+ }
+
+ protected function col_actions($data) {
+ global $OUTPUT;
+
+ $action = new \confirm_action(get_string('removecohortroleassignmentconfirm', 'tool_cohortroles'));
+ $url = new moodle_url($this->baseurl);
+ $url->params(array('removecohortroleassignment' => $data->id, 'sesskey' => sesskey()));
+ $pix = new \pix_icon('t/delete', get_string('removecohortroleassignment', 'tool_cohortroles'));
+ return $OUTPUT->action_link($url, '', $action, null, $pix);
+ }
+
+ /**
+ * Setup the headers for the table.
+ */
+ protected function define_table_columns() {
+ $extrafields = get_extra_user_fields($this->context);
+
+ // Define headers and columns.
+ $cols = array(
+ 'cohortname' => get_string('cohort', 'cohort'),
+ 'rolename' => get_string('role'),
+ 'fullname' => get_string('name'),
+ 'actions' => get_string('actions')
+ );
+
+ // Add headers for extra user fields.
+ foreach ($extrafields as $field) {
+ if (get_string_manager()->string_exists($field, 'moodle')) {
+ $cols[$field] = get_string($field);
+ } else {
+ $cols[$field] = $field;
+ }
+ }
+
+ // Add remaining headers.
+ $cols = array_merge($cols, array());
+
+ $this->define_columns(array_keys($cols));
+ $this->define_headers(array_values($cols));
+ }
+
+ /**
+ * Define table configs.
+ */
+ protected function define_table_configs() {
+ $this->collapsible(false);
+ $this->sortable(true, 'lastname', SORT_ASC);
+ $this->pageable(true);
+ }
+
+ /**
+ * Builds the SQL query.
+ *
+ * @param bool $count When true, return the count SQL.
+ * @return array containing sql to use and an array of params.
+ */
+ protected function get_sql_and_params($count = false) {
+ $fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, ';
+ $fields .= 'c.name as cohortname, c.idnumber as cohortidnumber, c.contextid as cohortcontextid, ';
+ $fields .= 'c.visible as cohortvisible, c.description as cohortdescription, ';
+
+ // Add extra user fields that we need for the graded user.
+ $extrafields = get_extra_user_fields($this->context);
+ foreach ($extrafields as $field) {
+ $fields .= 'u.' . $field . ', ';
+ }
+ $fields .= get_all_user_name_fields(true, 'u');
+
+ if ($count) {
+ $select = "COUNT(1)";
+ } else {
+ $select = "$fields";
+ }
+
+ $sql = "SELECT $select
+ FROM {tool_cohortroles} uca
+ JOIN {user} u ON u.id = uca.userid
+ JOIN {cohort} c ON c.id = uca.cohortid";
+ $params = array();
+
+ // Add order by if needed.
+ if (!$count && $sqlsort = $this->get_sql_sort()) {
+ $sql .= " ORDER BY " . $sqlsort;
+ }
+
+ return array($sql, $params);
+ }
+
+ /**
+ * Override the default implementation to set a decent heading level.
+ */
+ public function print_nothing_to_display() {
+ global $OUTPUT;
+ echo $this->render_reset_button();
+ $this->print_initials_bar();
+ echo $OUTPUT->heading(get_string('nothingtodisplay'), 4);
+ }
+
+ /**
+ * Query the DB.
+ *
+ * @param int $pagesize size of page for paginated displayed table.
+ * @param bool $useinitialsbar do you want to use the initials bar.
+ */
+ public function query_db($pagesize, $useinitialsbar = true) {
+ global $DB;
+
+ list($countsql, $countparams) = $this->get_sql_and_params(true);
+ list($sql, $params) = $this->get_sql_and_params();
+ $total = $DB->count_records_sql($countsql, $countparams);
+ $this->pagesize($pagesize, $total);
+ $this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
+
+ // Set initial bars.
+ if ($useinitialsbar) {
+ $this->initialbars($total > $pagesize);
+ }
+ }
+}
diff --git a/admin/tool/cohortroles/classes/task/cohort_role_sync.php b/admin/tool/cohortroles/classes/task/cohort_role_sync.php
new file mode 100644
index 00000000000..45a5e9e4035
--- /dev/null
+++ b/admin/tool/cohortroles/classes/task/cohort_role_sync.php
@@ -0,0 +1,48 @@
+.
+
+/**
+ * Scheduled task for syncing cohort roles.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_cohortroles\task;
+
+use core\task\scheduled_task;
+use tool_cohortroles\api;
+
+/**
+ * Scheduled task for syncing cohort roles.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_role_sync extends scheduled_task {
+ public function get_name() {
+ // Shown in admin screens.
+ return get_string('taskname', 'tool_cohortroles');
+ }
+
+ public function execute() {
+ mtrace('Sync cohort roles...');
+ $result = api::sync_all_cohort_roles();
+
+ mtrace('Added ' . count($result['rolesadded']));
+ mtrace('Removed ' . count($result['rolesremoved']));
+ }
+}
diff --git a/admin/tool/cohortroles/db/install.xml b/admin/tool/cohortroles/db/install.xml
new file mode 100644
index 00000000000..538ec1fe6a1
--- /dev/null
+++ b/admin/tool/cohortroles/db/install.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/tool/cohortroles/db/tasks.php b/admin/tool/cohortroles/db/tasks.php
new file mode 100644
index 00000000000..e5f5a627bb9
--- /dev/null
+++ b/admin/tool/cohortroles/db/tasks.php
@@ -0,0 +1,37 @@
+.
+
+/**
+ * Tasks definitions.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+ array(
+ 'classname' => 'tool_cohortroles\task\cohort_role_sync',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => '*',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
+);
diff --git a/admin/tool/cohortroles/index.php b/admin/tool/cohortroles/index.php
new file mode 100644
index 00000000000..dfcbb502fca
--- /dev/null
+++ b/admin/tool/cohortroles/index.php
@@ -0,0 +1,98 @@
+.
+
+/**
+ * Assign roles for a user.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+$removeid = optional_param('removecohortroleassignment', 0, PARAM_INT);
+
+admin_externalpage_setup('toolcohortroles');
+$context = context_system::instance();
+
+$pageurl = new moodle_url('/admin/tool/cohortroles/index.php');
+
+$renderer = $PAGE->get_renderer('core');
+
+echo $OUTPUT->header();
+$title = get_string('assignroletocohort', 'tool_cohortroles');
+echo $OUTPUT->heading($title);
+
+$form = new tool_cohortroles\form\assign_role_cohort();
+
+if ($removeid) {
+ require_sesskey();
+
+ $result = \tool_cohortroles\api::delete_cohort_role_assignment($removeid);
+ if ($result) {
+ $notification = get_string('cohortroleassignmentremoved', 'tool_cohortroles');
+ echo $OUTPUT->notify_success($notification);
+ } else {
+ $notification = get_string('cohortroleassignmentnotremoved', 'tool_cohortroles');
+ echo $OUTPUT->notify_problem($notification);
+ }
+ echo $OUTPUT->continue_button(new moodle_url($pageurl));
+} else if ($data = $form->get_data()) {
+ require_sesskey();
+ // We must create them all or none.
+ $saved = 0;
+ foreach ($data->userids as $userid) {
+ if (empty($data->cohortids)) {
+ $data->cohortids = array();
+ }
+ foreach ($data->cohortids as $cohortid) {
+ $params = (object) array('userid' => $userid, 'cohortid' => $cohortid, 'roleid' => $data->roleid);
+ $result = \tool_cohortroles\api::create_cohort_role_assignment($params);
+ if ($result) {
+ $saved++;
+ }
+ }
+ }
+ if ($saved == 0) {
+ $notification = get_string('nocohortroleassignmentssaved', 'tool_cohortroles');
+ echo $OUTPUT->notify_problem($notification);
+ } else if ($saved == 1) {
+ $notification = get_string('onecohortroleassignmentsaved', 'tool_cohortroles');
+ echo $OUTPUT->notify_success($notification);
+ } else {
+ $notification = get_string('acohortroleassignmentssaved', 'tool_cohortroles', $saved);
+ echo $OUTPUT->notify_success($notification);
+ }
+
+ echo $OUTPUT->continue_button(new moodle_url($pageurl));
+} else {
+ $form->display();
+
+ $title = get_string('existingcohortroles', 'tool_cohortroles');
+ echo $OUTPUT->heading($title);
+ $url = new moodle_url('/admin/tool/cohortroles/index.php');
+ $table = new tool_cohortroles\output\cohort_role_assignments_table(uniqid(), $url);
+ echo $table->out(50, true);
+
+ echo $OUTPUT->spacer();
+ echo $OUTPUT->notify_message(get_string('backgroundsync', 'tool_cohortroles'));
+}
+
+echo $OUTPUT->footer();
+
diff --git a/admin/tool/cohortroles/lang/en/tool_cohortroles.php b/admin/tool/cohortroles/lang/en/tool_cohortroles.php
new file mode 100644
index 00000000000..40569192962
--- /dev/null
+++ b/admin/tool/cohortroles/lang/en/tool_cohortroles.php
@@ -0,0 +1,44 @@
+.
+
+/**
+ * Strings for component 'tool_userroles', language 'en'
+ *
+ * @package tool_userroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['acohortroleassignmentssaved'] = '{$a} cohort role assignments were saved.';
+$string['assign'] = 'Assign';
+$string['assignroletocohort'] = 'Assign role to users in the context of all members of these cohorts';
+$string['backgroundsync'] = 'Note: Roles assigned on this page will not take affect immediately, the changes to role assignments will be made by a background task.';
+$string['cohortroleassignmentremoved'] = 'The cohort role assignment was removed.';
+$string['cohortroleassignmentnotremoved'] = 'The cohort role assignment was not removed.';
+$string['cohortroles'] = 'Cohort roles';
+$string['existingcohortroles'] = 'Existing cohort role assignments';
+$string['managecohortroles'] = 'Assign user roles for entire cohort';
+$string['noassignableroles'] = 'No roles can be assigned at a user context level. Manage roles';
+$string['nocohortroleassignmentssaved'] = 'No cohort role assignments were saved.';
+$string['onecohortroleassignmentsaved'] = 'One cohort role assignment was saved.';
+$string['pluginname'] = 'Cohort roles management';
+$string['removecohortroleassignment'] = 'Remove cohort role assignment';
+$string['removecohortroleassignmentconfirm'] = 'Are you sure you want to remove this cohort role assignment? This role will be removed for this user in all other user contexts.';
+$string['selectcohorts'] = 'Select cohorts';
+$string['selectrole'] = 'Select role';
+$string['selectusers'] = 'Select users to assign role';
+$string['taskname'] = 'Sync cohort role assignments';
+$string['thisuserroles'] = 'Roles assigned relative to this user';
diff --git a/admin/tool/cohortroles/settings.php b/admin/tool/cohortroles/settings.php
new file mode 100644
index 00000000000..98613c15042
--- /dev/null
+++ b/admin/tool/cohortroles/settings.php
@@ -0,0 +1,27 @@
+.
+
+/**
+ * Link to user roles management.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+$str = get_string('managecohortroles', 'tool_cohortroles');
+$ADMIN->add('roles', new admin_externalpage('toolcohortroles', $str, '/admin/tool/cohortroles/index.php', 'moodle/role:manage'));
diff --git a/admin/tool/cohortroles/templates/cohort-in-list.mustache b/admin/tool/cohortroles/templates/cohort-in-list.mustache
new file mode 100644
index 00000000000..971065a0da1
--- /dev/null
+++ b/admin/tool/cohortroles/templates/cohort-in-list.mustache
@@ -0,0 +1,43 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template tool_cohortroles/cohort-in-list
+
+ Moodle template for displaying a cohort in a list.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * id cohort id field
+ * name cohort name field
+ * idnumber cohort idnumber field
+ * description cohort description field
+ * visible cohort visible field
+
+ Example context (json):
+ { "id": "1",
+ "name": "Cohort 1",
+ "visible": true,
+ "idnumber": "014",
+ "description": "Some users"
+ }
+}}
+{{> tool_lp/form-cohort-selector-suggestion }}
diff --git a/admin/tool/cohortroles/tests/api_test.php b/admin/tool/cohortroles/tests/api_test.php
new file mode 100644
index 00000000000..ab08e5ec732
--- /dev/null
+++ b/admin/tool/cohortroles/tests/api_test.php
@@ -0,0 +1,243 @@
+.
+
+/**
+ * API tests.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use tool_cohortroles\api;
+
+/**
+ * API tests.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_cohortroles_api_testcase extends advanced_testcase {
+ /** @var stdClass $cohort */
+ protected $cohort = null;
+
+ /** @var stdClass $userassignto */
+ protected $userassignto = null;
+
+ /** @var stdClass $userassignover */
+ protected $userassignover = null;
+
+ /** @var stdClass $role */
+ protected $role = null;
+
+ /**
+ * Setup function- we will create a course and add an assign instance to it.
+ */
+ protected function setUp() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ // Create some users.
+ $this->cohort = $this->getDataGenerator()->create_cohort();
+ $this->userassignto = $this->getDataGenerator()->create_user();
+ $this->userassignover = $this->getDataGenerator()->create_user();
+ $this->roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
+ cohort_add_member($this->cohort->id, $this->userassignover->id);
+ }
+
+
+ public function test_create_cohort_role_assignment_without_permission() {
+ $this->setExpectedException('required_capability_exception');
+ $this->setUser($this->userassignto);
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ api::create_cohort_role_assignment($params);
+ }
+
+ public function test_create_cohort_role_assignment_with_invalid_data() {
+ $this->setExpectedException('tool_lp\invalid_persistent_exception');
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => -8,
+ 'cohortid' => $this->cohort->id
+ );
+ api::create_cohort_role_assignment($params);
+ }
+
+ public function test_create_cohort_role_assignment() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $this->assertNotEmpty($result->get_id());
+ $this->assertEquals($result->get_userid(), $this->userassignto->id);
+ $this->assertEquals($result->get_roleid(), $this->roleid);
+ $this->assertEquals($result->get_cohortid(), $this->cohort->id);
+ }
+
+ public function test_delete_cohort_role_assignment_without_permission() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $this->setExpectedException('required_capability_exception');
+ $this->setUser($this->userassignto);
+ api::delete_cohort_role_assignment($result->get_id());
+ }
+
+ public function test_delete_cohort_role_assignment_with_invalid_data() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $this->setExpectedException('dml_missing_record_exception');
+ api::delete_cohort_role_assignment($result->get_id() + 1);
+ }
+
+ public function test_delete_cohort_role_assignment() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $worked = api::delete_cohort_role_assignment($result->get_id());
+ $this->assertTrue($worked);
+ }
+
+ public function test_list_cohort_role_assignments() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+
+ $list = api::list_cohort_role_assignments();
+ $list[0]->is_valid();
+ $this->assertEquals($list[0], $result);
+ }
+
+ public function test_count_cohort_role_assignments() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+
+ $count = api::count_cohort_role_assignments();
+ $this->assertEquals($count, 1);
+ }
+
+ public function test_sync_all_cohort_roles() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+
+ // Verify roles are assigned when users enter the cohort.
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $rolesremoved = array();
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Verify roles are removed when users leave the cohort.
+ cohort_remove_member($this->cohort->id, $this->userassignover->id);
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array();
+ $rolesremoved = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Verify roles assigned by any other component are not removed.
+ $usercontext = context_user::instance($this->userassignover->id);
+ role_assign($this->roleid, $this->userassignto->id, $usercontext->id);
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array();
+ $rolesremoved = array();
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Remove manual role assignment.
+ role_unassign($this->roleid, $this->userassignto->id, $usercontext->id);
+ // Add someone to the cohort again...
+ cohort_add_member($this->cohort->id, $this->userassignover->id);
+ $sync = api::sync_all_cohort_roles();
+ $rolesadded = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $rolesremoved = array();
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Verify no fatal errors when a cohort is deleted.
+ cohort_delete_cohort($this->cohort);
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array();
+ $rolesremoved = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+ }
+
+}
diff --git a/admin/tool/cohortroles/version.php b/admin/tool/cohortroles/version.php
new file mode 100644
index 00000000000..bf9fb21f408
--- /dev/null
+++ b/admin/tool/cohortroles/version.php
@@ -0,0 +1,34 @@
+.
+
+/**
+ * Plugin version info
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+$plugin->version = 2015111018; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires = 2014110400; // Requires this Moodle version.
+$plugin->component = 'tool_cohortroles'; // Full name of the plugin (used for diagnostics).
+
+$plugin->dependencies = array(
+ 'tool_lp' => ANY_VERSION
+);
diff --git a/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache b/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache
index 881a56bdb29..fafe24f89c6 100644
--- a/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache
+++ b/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache
@@ -45,5 +45,5 @@
{{#idnumber}}
{{idnumber}}
{{/idnumber}}
- {{contextname}}
+ ( {{contextname}} )