Commit 027ffc0d authored by Richard Mansfield's avatar Richard Mansfield
Browse files

Add more flexible group jointypes (bug #610690)



The four existing jointypes, 'open', 'controlled', 'request' and
'invite' are mutually exclusive, but they don't need to be so strict.
This patch introduces more flexibility in the way groups allow new
members to join.

* Group admins can always send membership invitations to a group, even
  if it's open or controlled
* Membership requests can be enabled for any group unless it has open
  membership.
* The grouptype now determines the set of roles available to a group,
  but no longer restricts the available join types.

The db upgrade will preserve existing behaviour apart from enabling
invitations on open, request, and controlled groups.

Change-Id: I8bb0940a37f3c0c36366c1d5b8d27e8b9914a7e3
Signed-off-by: default avatarRichard Mansfield <richard.mansfield@catalyst.net.nz>
parent 5436cc00
......@@ -44,7 +44,10 @@ $ALLOWEDKEYS = array(
'shortname',
'displayname',
'description',
'grouptype',
'open',
'controlled',
'request',
'roles',
'public',
);
if ($USER->get('admin')) {
......@@ -54,15 +57,10 @@ if ($USER->get('admin')) {
$MANDATORYFIELDS = array(
'shortname',
'displayname',
'grouptype',
'roles',
);
$UPDATES = array(); // During validation, remember which group already exist
$JOINTYPES = array();
foreach (group_get_grouptypes() as $type) {
safe_require('grouptype', $type);
$JOINTYPES[$type] = call_static_method('GroupType' . $type, 'allowed_join_types');
}
$GROUPTYPES = group_get_grouptype_options();
$form = array(
'name' => 'uploadcsv',
......@@ -99,7 +97,7 @@ $form = array(
* @param array $values The values submitted
*/
function uploadcsv_validate(Pieform $form, $values) {
global $CSVDATA, $ALLOWEDKEYS, $MANDATORYFIELDS, $JOINTYPES, $FORMAT, $USER, $UPDATES;
global $CSVDATA, $ALLOWEDKEYS, $MANDATORYFIELDS, $GROUPTYPES, $FORMAT, $USER, $UPDATES;
// Don't even start attempting to parse if there are previous errors
if ($form->has_errors()) {
......@@ -153,7 +151,10 @@ function uploadcsv_validate(Pieform $form, $values) {
$shortname = $line[$formatkeylookup['shortname']];
$displayname = $line[$formatkeylookup['displayname']];
$grouptype = $line[$formatkeylookup['grouptype']];
$grouptype = $line[$formatkeylookup['roles']];
$open = isset($formatkeylookup['open']) && !empty($line[$formatkeylookup['open']]);
$controlled = isset($formatkeylookup['controlled']) && !empty($line[$formatkeylookup['controlled']]);
$request = isset($formatkeylookup['request']) && !empty($line[$formatkeylookup['request']]);
if (!preg_match('/^[a-zA-Z0-9_.-]{2,255}$/', $shortname)) {
$csverrors->add($i, get_string('uploadgroupcsverrorinvalidshortname', 'admin', $i, $shortname));
......@@ -172,7 +173,7 @@ function uploadcsv_validate(Pieform $form, $values) {
$shortnames[$shortname] = array(
'shortname' => $shortname,
'displayname' => $displayname,
'grouptype' => $grouptype,
'roles' => $grouptype,
'lineno' => $i,
'raw' => $line,
);
......@@ -203,16 +204,16 @@ function uploadcsv_validate(Pieform $form, $values) {
}
$displaynames[strtolower($displayname)] = 1;
$groupjointype = split('/', $grouptype);
if (count($groupjointype) != 2) {
if (!isset($GROUPTYPES[$grouptype])) {
$csverrors->add($i, get_string('uploadgroupcsverrorinvalidgrouptype', 'admin', $i, $grouptype));
}
else {
if (!isset($JOINTYPES[$groupjointype[0]]) || !in_array($groupjointype[1], $JOINTYPES[$groupjointype[0]])) {
$csverrors->add($i, get_string('uploadgroupcsverrorinvalidgrouptype', 'admin', $i, $grouptype));
}
}
if ($open && $controlled) {
$csverrors->add($i, get_string('uploadgroupcsverroropencontrolled', 'admin', $i));
}
if ($open && $request) {
$csverrors->add($i, get_string('uploadgroupcsverroropenrequest', 'admin', $i));
}
if ($values['updategroups']) {
......@@ -259,16 +260,14 @@ function uploadcsv_submit(Pieform $form, $values) {
foreach ($CSVDATA as $record) {
$groupjointype = split('/', $record[$formatkeylookup['grouptype']]);
$group = new StdClass;
$group->name = $record[$formatkeylookup['displayname']];
$group->shortname = $record[$formatkeylookup['shortname']];
$group->institution = $institution;
$group->grouptype = $groupjointype[0];
$group->jointype = $groupjointype[1];
$group->grouptype = $record[$formatkeylookup['roles']];
foreach ($FORMAT as $field) {
if ($field == 'displayname' || $field == 'shortname' || $field == 'grouptype') {
if ($field == 'displayname' || $field == 'shortname' || $field == 'roles') {
continue;
}
$group->{$field} = $record[$formatkeylookup[$field]];
......@@ -316,23 +315,21 @@ function uploadcsv_submit(Pieform $form, $values) {
}
$grouptypes = "<ul class=fieldslist>\n";
foreach ($JOINTYPES as $grouptype => $jointypes) {
foreach ($jointypes as $type) {
$grouptypes .= '<li>' . hsc($grouptype) . '/' . hsc($type) . "</li>\n";
}
foreach (array_keys($GROUPTYPES) as $grouptype) {
$grouptypes .= '<li>' . hsc($grouptype) . "</li>\n";
}
$grouptypes .= "<div class=cl></div></ul>\n";
$fields = "<ul class=fieldslist>\n";
foreach ($ALLOWEDKEYS as $type) {
$helplink = '';
if ($type == 'grouptype' || $type == 'public' || $type == 'usersautoadded') {
$helplink = get_help_icon('core', 'groups', 'creategroup', $type);
if ($type == 'public' || $type == 'usersautoadded') {
$helplink = get_help_icon('core', 'groups', 'editgroup', $type);
}
$fields .= '<li>' . hsc($type) . $helplink . "</li>\n";
}
$fields .= "<div class=cl></div></ul>\n";
$uploadcsvpagedescription = get_string('uploadgroupcsvpagedescription2', 'admin', get_help_icon('core', 'groups', 'creategroup', 'grouptype'), $grouptypes, $fields);
$uploadcsvpagedescription = get_string('uploadgroupcsvpagedescription2', 'admin', get_help_icon('core', 'groups', 'editgroup', 'grouptype'), $grouptypes, $fields);
$form = pieform($form);
......
......@@ -39,7 +39,7 @@ $data['message'] = null;
$initialgroups = param_integer_list('initialgroups', array());
$resultgroups = param_integer_list('resultgroups', array());
$userid = param_integer('userid');
$jointype = param_variable('jointype');
$addtype = param_variable('addtype');
// Prevent group membership changing done by ordinary members, Tutors can only
// add members to group and cannot remove anyone. Group admins can do anything.
......@@ -71,7 +71,7 @@ foreach ($allgroups as $groupid) {
}
}
if ($jointype == 'controlled') {
if ($addtype == 'add') {
db_begin();
//remove group membership
if ($groupstoremove = array_diff($initialgroups, $resultgroups)) {
......@@ -111,7 +111,7 @@ if ($jointype == 'controlled') {
activity_occurred('maharamessage', $n);
$data['message'] = get_string('changedgroupmembership', 'group');
}
elseif ($jointype == 'invite') {
else if ($addtype == 'invite') {
if ($groupstoadd = array_diff($resultgroups, $initialgroups)) {
foreach ($groupstoadd as $groupid) {
group_invite_user($groupdata[$groupid], $userid, $USER->get('id'));
......
......@@ -34,7 +34,7 @@ require(dirname(dirname(__FILE__)) . '/init.php');
$userid = param_integer('userid');
$groupdata = array();
$initialgroups = array('controlled' => array(), 'invite' => array());
$initialgroups = array('add' => array(), 'invite' => array());
/* Get (a) controlled membership groups,
(b) request membership groups where the displayed user has requested membership,
......@@ -60,7 +60,7 @@ $controlled = get_records_sql_array("SELECT g.*, gm.role,
JOIN {grouptype_roles} gtr ON (gtr.grouptype = g.grouptype AND gtr.role = gm.role)
LEFT JOIN {group_member_request} gmr ON (gmr.member = ? AND gmr.group = g.id)
WHERE gm.member = ?
AND (g.jointype = 'controlled' OR (g.jointype = 'request' AND gmr.member = ?))
AND (g.jointype = 'controlled' OR (g.request = 1 AND gmr.member = ?))
AND (gm.role = 'admin' OR gtr.see_submitted_views = 1)
AND g.deleted = 0", array($userid, $userid, $userid, $USER->get('id'), $userid));
......@@ -68,16 +68,16 @@ if ($controlled) {
foreach ($controlled as &$g) {
if ($g->member) {
$g->checked = true;
$initialgroups['controlled'][] = $g->id;
$initialgroups['add'][] = $g->id;
if ($g->role != 'admin') {
$g->disabled = true;
}
}
}
$groupdata['controlled'] = $controlled;
$groupdata['add'] = $controlled;
}
/* Get 'Invite olny' groups where the logged in user is a group admin.
/* Get groups where the logged in user is a group admin.
@return array A data structure containing results looking like ...
* $results = array(
* array(
......@@ -95,7 +95,6 @@ $invite = get_records_sql_array("SELECT g.*, gm.role,
FROM {group} g
JOIN {group_member} gm ON (gm.group = g.id)
WHERE gm.member = ?
AND g.jointype = 'invite'
AND gm.role = 'admin'
AND g.deleted = 0", array($userid, $userid, $USER->get('id')));
......
......@@ -67,47 +67,106 @@ else {
);
}
$elements = array();
$elements['name'] = array(
$form = array(
'name' => 'editgroup',
'plugintype' => 'core',
'pluginname' => 'groups',
'elements' => array(
'name' => array(
'type' => 'text',
'title' => get_string('groupname', 'group'),
'rules' => array( 'required' => true, 'maxlength' => 128 ),
'defaultvalue' => $group_data->name);
$elements['description'] = array(
'defaultvalue' => $group_data->name,
),
'description' => array(
'type' => 'wysiwyg',
'title' => get_string('groupdescription', 'group'),
'rules' => array('maxlength' => 65536),
'rows' => 10,
'cols' => 55,
'defaultvalue' => $group_data->description);
'defaultvalue' => $group_data->description,
),
'settings' => array(
'type' => 'fieldset',
'collapsible' => true,
'collapsed' => false,
'legend' => get_string('settings'),
'elements' => array(),
),
'submit' => array(
'type' => 'submitcancel',
'value' => array(get_string('savegroup', 'group'), get_string('cancel'))
),
),
);
$grouptypeoptions = group_get_grouptype_options($group_data->grouptype);
$currenttype = $group_data->grouptype . '.' . $group_data->jointype;
if (!isset($grouptypeoptions[$currenttype])) {
// The user can't create groups of this type. Probably a non-staff user
// who's been promoted to admin of a controlled group. Just don't let
// them change it.
$grouptypeoptions = array($currenttype => get_string('membershiptype.' . $group_data->jointype, 'group'));
$elements = array();
$cancreatecontrolled = $USER->get('admin') || $USER->get('staff')
|| $USER->is_institutional_admin() || $USER->is_institutional_staff();
$elements['open'] = array(
'type' => 'checkbox',
'title' => get_string('Open', 'group'),
'description' => get_string('opendescription', 'group'),
'defaultvalue' => $group_data->jointype == 'open',
'disabled' => !$cancreatecontrolled && $group_data->jointype == 'controlled',
);
if ($cancreatecontrolled || $group_data->jointype == 'controlled') {
$elements['controlled'] = array(
'type' => 'checkbox',
'title' => get_string('Controlled', 'group'),
'description' => get_string('controlleddescription', 'group'),
'defaultvalue' => $group_data->jointype == 'controlled',
'disabled' => !$cancreatecontrolled,
);
}
else {
$form['elements']['controlled'] = array(
'type' => 'hidden',
'value' => $group_data->jointype == 'controlled',
);
}
$elements['request'] = array(
'type' => 'checkbox',
'title' => get_string('request', 'group'),
'description' => get_string('requestdescription', 'group'),
'defaultvalue' => $group_data->jointype != 'open' && $group_data->request,
'disabled' => $group_data->jointype == 'open',
);
// The grouptype determines the allowed roles
$grouptypeoptions = group_get_grouptype_options($group_data->grouptype);
$elements['grouptype'] = array(
'type' => 'select',
'title' => get_string('grouptype', 'group'),
'title' => get_string('Roles', 'group'),
'options' => $grouptypeoptions,
'defaultvalue' => $currenttype,
'defaultvalue' => $group_data->grouptype,
'help' => true
);
// If it's a new group and a grouptype was passed in as a parameter, hide it in the form.
// Hide the grouptype option if it was passed in as a parameter, if the user
// isn't allowed to change it, or if there's only one option.
if (!$id) {
$grouptypeparam = param_alphanumext('grouptype', 0);
if (isset($grouptypeoptions[$grouptypeparam])) {
$elements['grouptype'] = array(
'type' => 'hidden',
'value' => $grouptypeparam,
);
$group_data->grouptype = $grouptypeparam;
$forcegrouptype = true;
}
}
else if (!isset($grouptypeoptions[$group_data->grouptype])) {
// The user can't create groups of this type. Probably a non-staff user
// who's been promoted to admin of a controlled group.
$forcegrouptype = true;
}
if (!empty($forcegrouptype) || count($grouptypeoptions) < 2) {
$form['elements']['grouptype'] = array(
'type' => 'hidden',
'value' => $group_data->grouptype,
);
}
if (get_config('allowgroupcategories')
&& $groupcategories = get_records_menu('group_category','','','displayorder', 'id,title')
......@@ -121,7 +180,7 @@ if (get_config('allowgroupcategories')
// If it's a new group & the category was passed as a parameter, hide it in the form.
$groupcategoryparam = param_integer('category', 0);
if (!$id && isset($groupcategories[$groupcategoryparam])) {
$elements['category'] = array(
$form['elements']['category'] = array(
'type' => 'hidden',
'value' => $groupcategoryparam,
);
......@@ -156,16 +215,8 @@ $elements['viewnotify'] = array(
'defaultvalue' => $group_data->viewnotify
);
$elements['submit'] = array(
'type' => 'submitcancel',
'value' => array(get_string('savegroup', 'group'), get_string('cancel')));
$editgroup = pieform(array(
'name' => 'editgroup',
'method' => 'post',
'plugintype' => 'core',
'pluginname' => 'groups',
'elements' => $elements));
$form['elements']['settings']['elements'] = $elements;
$editgroup = pieform($form);
function editgroup_validate(Pieform $form, $values) {
global $group_data;
......@@ -177,6 +228,14 @@ function editgroup_validate(Pieform $form, $values) {
}
}
}
if (!empty($values['open'])) {
if (!empty($values['controlled'])) {
$form->set_error('open', get_string('membershipopencontrolled', 'group'));
}
if (!empty($values['request'])) {
$form->set_error('request', get_string('membershipopenrequest', 'group'));
}
}
}
function editgroup_cancel_submit() {
......@@ -186,23 +245,24 @@ function editgroup_cancel_submit() {
function editgroup_submit(Pieform $form, $values) {
global $USER, $SESSION, $group_data;
db_begin();
list($grouptype, $jointype) = explode('.', $values['grouptype']);
$values['public'] = (isset($values['public'])) ? $values['public'] : 0;
$values['usersautoadded'] = (isset($values['usersautoadded'])) ? $values['usersautoadded'] : 0;
$newvalues = array(
'name' => $group_data->name == $values['name'] ? $values['name'] : trim($values['name']),
'description' => $values['description'],
'grouptype' => $grouptype,
'grouptype' => $values['grouptype'],
'category' => empty($values['category']) ? null : intval($values['category']),
'jointype' => $jointype,
'open' => intval($values['open']),
'controlled' => intval($values['controlled']),
'request' => intval($values['request']),
'usersautoadded' => intval($values['usersautoadded']),
'public' => intval($values['public']),
'viewnotify' => intval($values['viewnotify']),
);
db_begin();
if ($group_data->id) {
$newvalues['id'] = $group_data->id;
group_update((object)$newvalues);
......@@ -220,7 +280,29 @@ function editgroup_submit(Pieform $form, $values) {
redirect('/group/view.php?id=' . $group_data->id);
}
$smarty = smarty();
$js = '
$j(function() {
$j("#editgroup_controlled").click(function() {
if ($(this).checked) {
$j("#editgroup_request").removeAttr("disabled");
$j("#editgroup_open").removeAttr("checked");
}
});
$j("#editgroup_open").click(function() {
if ($(this).checked) {
$j("#editgroup_controlled").removeAttr("checked");
$j("#editgroup_request").removeAttr("checked");
$j("#editgroup_request").attr("disabled", true);
}
else {
$j("#editgroup_request").removeAttr("disabled");
}
});
});
';
$smarty = smarty(array('jquery'));
$smarty->assign('form', $editgroup);
$smarty->assign('PAGEHEADING', TITLE);
$smarty->assign('INLINEJAVASCRIPT', $js);
$smarty->display('form.tpl');
......@@ -93,9 +93,11 @@ if ($groups['data']) {
$groupids[] = $group->id;
}
$groups['data'] = get_records_sql_array(
"SELECT g1.id, g1.name, g1.description, g1.public, g1.jointype, g1.grouptype, g1.role, g1.membershiptype, g1.membercount, COUNT(gmr.member) AS requests
"SELECT g1.id, g1.name, g1.description, g1.public, g1.jointype, g1.request, g1.grouptype, g1.role, g1.membershiptype,
g1.membercount, COUNT(gmr.member) AS requests
FROM (
SELECT g.id, g.name, g.description, g.public, g.jointype, g.grouptype, t.role, t.membershiptype, COUNT(gm.member) AS membercount
SELECT g.id, g.name, g.description, g.public, g.jointype, g.request, g.grouptype, t.role, t.membershiptype,
COUNT(gm.member) AS membercount
FROM {group} g
LEFT JOIN {group_member} gm ON (gm.group = g.id)
LEFT JOIN (
......@@ -116,10 +118,11 @@ if ($groups['data']) {
INNER JOIN {group_member_request} gmr ON (gmr.group = g.id AND gmr.member = ?)
) t ON t.id = g.id
WHERE g.id IN (" . implode($groupids, ',') . ')
GROUP BY g.id, g.name, g.description, g.public, g.jointype, g.grouptype, t.role, t.membershiptype
GROUP BY g.id, g.name, g.description, g.public, g.jointype, g.request, g.grouptype, t.role, t.membershiptype
) g1
LEFT JOIN {group_member_request} gmr ON (gmr.group = g1.id)
GROUP BY g1.id, g1.name, g1.description, g1.public, g1.jointype, g1.grouptype, g1.role, g1.membershiptype, g1.membercount
GROUP BY g1.id, g1.name, g1.description, g1.public, g1.jointype, g1.request, g1.grouptype, g1.role, g1.membershiptype,
g1.membercount
ORDER BY g1.name',
array($USER->get('id'), $USER->get('id'), $USER->get('id'), $USER->get('id'))
);
......
......@@ -41,8 +41,7 @@ if (!$user) {
throw new UserNotFoundException(get_string('usernotfound', 'group', $userid));
}
if ($group->jointype != 'invite'
|| group_user_access($groupid) != 'admin') {
if (group_user_access($groupid) != 'admin') {
throw new AccessDeniedException(get_string('cannotinvitetogroup', 'group'));
}
......
......@@ -44,10 +44,6 @@ if ($role != 'admin') {
throw new AccessDeniedException();
}
if ($group->jointype != 'invite') {
redirect(get_config('wwwroot') . 'group/members.php?id=' . GROUP);
}
define('TITLE', $group->name . ' - ' . get_string('sendinvitations', 'group'));
$form = pieform(array(
......
......@@ -75,13 +75,11 @@ list($html, $pagination, $count, $offset, $membershiptype) = group_get_membersea
// Type-specific instructions
$instructions = '';
if ('admin' == $role) {
if ('invite' == $group->jointype) {
$url = get_config('wwwroot') . 'group/inviteusers.php?id=' . GROUP;
$instructions = get_string('membersdescription:invite', 'group', $url);
}
else if ('controlled' == $group->jointype) {
$url = get_config('wwwroot') . 'group/inviteusers.php?id=' . GROUP;
$instructions = get_string('invitemembersdescription', 'group', $url);
if ('controlled' == $group->jointype) {
$url = get_config('wwwroot') . 'group/addmembers.php?id=' . GROUP;
$instructions = get_string('membersdescription:controlled', 'group', $url);
$instructions .= ' ' . get_string('membersdescription:controlled', 'group', $url);
}
}
......@@ -128,13 +126,13 @@ if ($role == 'admin') {
'name' => get_string('current', 'group'),
'link' => empty($membershiptype) ? '' : $CFG->wwwroot.'group/members.php?id='.$group->id
);
if ($group->jointype == 'request' && count_records('group_member_request', 'group', $group->id)) {
if ($group->request && count_records('group_member_request', 'group', $group->id)) {
$membershiptypes[] = array(
'name' => get_string('requests', 'group'),
'link' => $membershiptype == 'request' ? '' : $CFG->wwwroot.'group/members.php?id='.$group->id.'&membershiptype=request'
);
}
if ($group->jointype == 'invite' && count_records('group_member_invite', 'group', $group->id)) {
if (count_records('group_member_invite', 'group', $group->id)) {
$membershiptypes[] = array(
'name' => get_string('invites', 'group'),
'link' => $membershiptype == 'invite' ? '' : $CFG->wwwroot.'group/members.php?id='.$group->id.'&membershiptype=invite'
......
......@@ -36,7 +36,7 @@ $returnto = param_alpha('returnto', 'mygroups');
define('GROUP', $groupid);
$group = group_current_group();
if ($group->jointype != 'request'
if (!$group->request
|| record_exists('group_member', 'group', $groupid, 'member', $USER->get('id'))
|| record_exists('group_member_request', 'group', $groupid, 'member', $USER->get('id'))) {
throw new AccessDeniedException(get_string('cannotrequestjoingroup', 'group'));
......
......@@ -58,18 +58,17 @@ if ($USER->is_logged_in()) {
}
$group->canleave = group_user_can_leave($group->id);
}
else if ($group->jointype == 'invite'
and $invite = get_record('group_member_invite', 'group', $group->id, 'member', $USER->get('id'))) {
else if ($invite = get_record('group_member_invite', 'group', $group->id, 'member', $USER->get('id'))) {
$group->membershiptype = 'invite';
$group->invite = group_get_accept_form('invite', $group->id, $afterjoin);
}
else if ($group->jointype == 'request'
and $request = get_record('group_member_request', 'group', $group->id, 'member', $USER->get('id'))) {
$group->membershiptype = 'request';
}
else if ($group->jointype == 'open') {
$group->groupjoin = group_get_join_form('joingroup', $group->id, $afterjoin);
}
else if ($group->request
and $request = get_record('group_member_request', 'group', $group->id, 'member', $USER->get('id'))) {
$group->membershiptype = 'request';
}
}
$view = group_get_homepage_view($group->id);
......
......@@ -39,19 +39,6 @@ class PluginGrouptypeCourse extends PluginGrouptype {
class GroupTypeCourse extends GroupType {
public static function allowed_join_types($all=false) {
global $USER;
return self::user_allowed_join_types($USER, $all);
}
public static function user_allowed_join_types($user, $all=false) {
$jointypes = array();
if (defined('INSTALLER') || defined('CRON') || $all || $user->get('admin') || $user->get('staff') || $user->is_institutional_admin() || $user->is_institutional_staff()) {
$jointypes = array_merge($jointypes, array('controlled', 'request'));
}
return $jointypes;
}
public static function can_be_created_by_user() {
global $USER;
return $USER->get('admin') || $USER->get('staff') || $USER->is_institutional_admin()
......
......@@ -74,9 +74,6 @@ abstract class GroupType {
}
}
public static abstract function allowed_join_types();
public static abstract function user_allowed_join_types($user);
/**
* Returns whether the currently logged in user can create a group of this
* grouptype
......
......@@ -43,19 +43,6 @@ class PluginGrouptypeStandard extends PluginGrouptype {
class GroupTypeStandard extends GroupType {
public static function allowed_join_types($all=false) {
global $USER;
return self::user_allowed_join_types($USER, $all);
}
public static function user_allowed_join_types($user, $all=false) {
$jointypes = array('open', 'request', 'invite');
if (defined('INSTALLER') || defined('CRON') || $all || $user->get('admin') || $user->get('staff') || $user->is_institutional_admin() || $user->is_institutional_staff()) {
$jointypes[] = 'controlled';
}
return $jointypes;
}