diff --git a/CHANGES.rst b/CHANGES.rst index 145b3c797c..a84e79c19e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,11 @@ Changelog https://github.com/plone/Products.CMFPlone/issues/290 [khink] +- PLIP 10359: Migrate usergroups controlpanel to ``z3c.form`` and move it from + plone.app.controlpanel to Products.CMFPlone. Fix and extend tests and add + robot tests. + [ferewuz] + 5.0a3 (2014-11-01) ------------------ diff --git a/Products/CMFPlone/controlpanel/README.rst b/Products/CMFPlone/controlpanel/README.rst index c6b73ed5b3..718c1ddcf0 100644 --- a/Products/CMFPlone/controlpanel/README.rst +++ b/Products/CMFPlone/controlpanel/README.rst @@ -149,7 +149,7 @@ Overview Control Panel >>> tz_settings = registry.forInterface(IDateAndTimeSchema, prefix='plone') >>> tz_settings.portal_timezone = 'UTC' - + Markup Control Panel ------------------------ @@ -158,3 +158,13 @@ Markup Control Panel >>> markup_settings.default_type = 'text/html' >>> markup_settings.allowed_types = ('text/html', 'text/x-web-textile') + + +User and Groups Control Panel +------------------ + + >>> from Products.CMFPlone.interfaces import IUserGroupsSettingsSchema + >>> usergroups_settings = registry.forInterface(IUserGroupsSettingsSchema, prefix='plone') + + >>> usergroups_settings.many_groups = False + >>> usergroups_settings.many_users = False diff --git a/Products/CMFPlone/controlpanel/bbb/configure.zcml b/Products/CMFPlone/controlpanel/bbb/configure.zcml index e4510d3544..e669463729 100644 --- a/Products/CMFPlone/controlpanel/bbb/configure.zcml +++ b/Products/CMFPlone/controlpanel/bbb/configure.zcml @@ -11,5 +11,6 @@ + diff --git a/Products/CMFPlone/controlpanel/bbb/usergroups.py b/Products/CMFPlone/controlpanel/bbb/usergroups.py new file mode 100644 index 0000000000..8379bee890 --- /dev/null +++ b/Products/CMFPlone/controlpanel/bbb/usergroups.py @@ -0,0 +1,35 @@ +from zope.component import getAdapter +from zope.site.hooks import getSite +from zope.component import adapts +from zope.interface import implements +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.interfaces import IUserGroupsSettingsSchema +from Products.CMFPlone.interfaces import IPloneSiteRoot + + +class UserGroupsSettingsControlPanelAdapter(object): + + adapts(IPloneSiteRoot) + implements(IUserGroupsSettingsSchema) + + def __init__(self, context): + self.context = context + self.portal = getSite() + pprop = getToolByName(context, 'portal_properties') + self.context = pprop.site_properties + + def get_many_groups(self): + return self.context.many_groups + + def set_many_groups(self, value): + self.context.many_groups = value + + many_groups = property(get_many_groups, set_many_groups) + + def get_many_users(self): + return self.context.many_users + + def set_many_users(self, value): + self.context.many_users = value + + many_users = property(get_many_users, set_many_users) diff --git a/Products/CMFPlone/controlpanel/browser/configure.zcml b/Products/CMFPlone/controlpanel/browser/configure.zcml index b90a6f21ca..67f140cfc7 100644 --- a/Products/CMFPlone/controlpanel/browser/configure.zcml +++ b/Products/CMFPlone/controlpanel/browser/configure.zcml @@ -123,4 +123,52 @@ permission="plone.app.controlpanel.Markup" /> + + + + + + + + + + + + + diff --git a/Products/CMFPlone/controlpanel/browser/controlpanel_usergroups_layout.pt b/Products/CMFPlone/controlpanel/browser/controlpanel_usergroups_layout.pt new file mode 100644 index 0000000000..8617a14216 --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/controlpanel_usergroups_layout.pt @@ -0,0 +1,60 @@ + + + + +
+ + + Site Setup + › + +

View Title

+ +
+ Portal status message +
+ +
+ +
+ +
 
+
+ + +
+ +
+ + + diff --git a/Products/CMFPlone/controlpanel/browser/usergroups.py b/Products/CMFPlone/controlpanel/browser/usergroups.py new file mode 100644 index 0000000000..de14b36ef0 --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups.py @@ -0,0 +1,131 @@ +from Products.CMFCore.permissions import ManagePortal +from ZTUtils import make_query +from itertools import chain +from Acquisition import aq_inner +from Products.CMFPlone.utils import normalizeString +from zope.component import getAdapter +from Products.CMFPlone.interfaces import ISecuritySchema +from zope.component import getMultiAdapter +from AccessControl import getSecurityManager +from Products.Five.browser import BrowserView +from Products.CMFCore.utils import getToolByName +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from plone.z3cform import layout +from plone.autoform.form import AutoExtensibleForm +from Products.CMFPlone import PloneMessageFactory as _ +from z3c.form import form + +from Products.CMFPlone.interfaces import IUserGroupsSettingsSchema + + +class UserGroupsSettingsControlPanel(AutoExtensibleForm, form.EditForm): + schema = IUserGroupsSettingsSchema + id = "usergroupsettings-control-panel" + label = _("User/Groups settings") + description = _("User and groups settings for this site.") + form_name = _("User/Groups settings") + control_panel_view = "usergroups-controlpanel" + + +class ControlPanelFormWrapper(layout.FormWrapper): + """Use this form as the plone.z3cform layout wrapper to get the control + panel layout. + """ + + index = ViewPageTemplateFile('controlpanel_usergroups_layout.pt') + + +UserGroupsSettingsPanelView = layout.wrap_form( + UserGroupsSettingsControlPanel, ControlPanelFormWrapper +) + + +class UsersGroupsControlPanelView(BrowserView): + + @property + def portal_roles(self): + pmemb = getToolByName(aq_inner(self.context), 'portal_membership') + return [r for r in pmemb.getPortalRoles() if r != 'Owner'] + + @property + def many_users(self): + pprop = getToolByName(aq_inner(self.context), 'portal_properties') + return pprop.site_properties.many_users + + @property + def many_groups(self): + pprop = getToolByName(aq_inner(self.context), 'portal_properties') + return pprop.site_properties.many_groups + + @property + def email_as_username(self): + return getAdapter(aq_inner(self.context), ISecuritySchema).get_use_email_as_login() + + def makeQuery(self, **kw): + return make_query(**kw) + + def membershipSearch(self, searchString='', searchUsers=True, searchGroups=True, ignore=[]): + """Search for users and/or groups, returning actual member and group items + Replaces the now-deprecated prefs_user_groups_search.py script""" + groupResults = userResults = [] + + gtool = getToolByName(self, 'portal_groups') + mtool = getToolByName(self, 'portal_membership') + + searchView = getMultiAdapter((aq_inner(self.context), self.request), name='pas_search') + + if searchGroups: + groupResults = searchView.merge(chain(*[searchView.searchGroups(**{field: searchString}) for field in ['id', 'title']]), 'groupid') + groupResults = [gtool.getGroupById(g['id']) for g in groupResults if g['id'] not in ignore] + groupResults.sort(key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())) + + if searchUsers: + userResults = searchView.merge(chain(*[searchView.searchUsers(**{field: searchString}) for field in ['login', 'fullname', 'email']]), 'userid') + userResults = [mtool.getMemberById(u['id']) for u in userResults if u['id'] not in ignore] + userResults.sort(key=lambda x: x is not None and x.getProperty('fullname') is not None and normalizeString(x.getProperty('fullname')) or '') + + return groupResults + userResults + + def atoi(self, s): + try: + return int(s) + except ValueError: + return 0 + + @property + def is_zope_manager(self): + return getSecurityManager().checkPermission(ManagePortal, self.context) + + # The next two class methods implement the following truth table: + # + # MANY USERS/GROUPS SEARCHING CAN LIST USERS/GROUPS RESULT + # False False False Lists unavailable + # False False True Show all + # False True False Show matching + # False True True Show matching + # True False False Too many to list + # True False True Lists unavailable + # True True False Show matching + # True True True Show matching + + # TODO: Maybe have these methods return a text message (instead of a bool) + # corresponding to the actual result, e.g. "Too many to list", "Lists unavailable" + + @property + def show_group_listing_warning(self): + if not self.searchString: + acl = getToolByName(self, 'acl_users') + if acl.canListAllGroups(): + if self.many_groups: + return True + return False + + @property + def show_users_listing_warning(self): + if not self.searchString: + acl = getToolByName(self, 'acl_users') + # XXX Huh? Is canListAllUsers broken? + if not acl.canListAllUsers(): + if self.many_users: + return True + return False diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_groupdetails.pt b/Products/CMFPlone/controlpanel/browser/usergroups_groupdetails.pt new file mode 100644 index 0000000000..d6ce280054 --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_groupdetails.pt @@ -0,0 +1,254 @@ + + + + + + +
+ +
+ + +
 
+
+ +
+ Portal status message +
+ + +
+ + +

Create a Group

+
+ + +
+ + + (Required) + +
+ A unique identifier for the group. Can not be changed after creation. +
+ + +
+
+
+
+ +
+ + +

+ Edit Group Properties for unavailable +

+ +
+ Groups are logical collections of users, like departments and business units. + They are not directly related to permissions on a global level, you normally + use Roles for that - and let certain Groups have a particular role. +
+ +
+ + Up to Groups Overview + + +
+ +
+ Group Properties + +
+ + +
+ +
+ + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ +
+
+ + +
+
+
+
+ + +
+ +
+ + + diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_groupdetails.py b/Products/CMFPlone/controlpanel/browser/usergroups_groupdetails.py new file mode 100644 index 0000000000..08a0a10e4f --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_groupdetails.py @@ -0,0 +1,81 @@ +from Acquisition import aq_inner +from Products.CMFPlone.controlpanel.browser.usergroups import \ + UsersGroupsControlPanelView +from plone.protect import CheckAuthenticator +from Products.CMFPlone import PloneMessageFactory as _ +from Products.CMFCore.utils import getToolByName +from Products.statusmessages.interfaces import IStatusMessage + + +class GroupDetailsControlPanel(UsersGroupsControlPanelView): + + def __call__(self): + context = aq_inner(self.context) + + self.gtool = getToolByName(context, 'portal_groups') + self.gdtool = getToolByName(context, 'portal_groupdata') + self.regtool = getToolByName(context, 'portal_registration') + self.groupname = getattr(self.request, 'groupname', None) + self.grouproles = self.request.set('grouproles', []) + self.group = self.gtool.getGroupById(self.groupname) + self.grouptitle = self.groupname + if self.group is not None: + self.grouptitle = self.group.getGroupTitleOrName() + + self.request.set('grouproles', self.group.getRoles() if self.group else []) + + submitted = self.request.form.get('form.submitted', False) + if submitted: + CheckAuthenticator(self.request) + + msg = _(u'No changes made.') + self.group = None + + title = self.request.form.get('title', None) + description = self.request.form.get('description', None) + addname = self.request.form.get('addname', None) + + if addname: + if not self.regtool.isMemberIdAllowed(addname): + msg = _(u'The group name you entered is not valid.') + IStatusMessage(self.request).add(msg, 'error') + return self.index() + + success = self.gtool.addGroup(addname, (), (), title=title, + description=description, + REQUEST=self.request) + if not success: + msg = _(u'Could not add group ${name}, perhaps a user or group with ' + u'this name already exists.', mapping={u'name' : addname}) + IStatusMessage(self.request).add(msg, 'error') + return self.index() + + self.group = self.gtool.getGroupById(addname) + msg = _(u'Group ${name} has been added.', + mapping={u'name' : addname}) + + elif self.groupname: + self.gtool.editGroup(self.groupname, roles=None, groups=None, + title=title, description=description, + REQUEST=context.REQUEST) + self.group = self.gtool.getGroupById(self.groupname) + msg = _(u'Changes saved.') + + else: + msg = _(u'Group name required.') + + processed = {} + for id, property in self.gdtool.propertyItems(): + processed[id] = self.request.get(id, None) + + if self.group: + # for what reason ever, the very first group created does not exist + self.group.setGroupProperties(processed) + + IStatusMessage(self.request).add(msg, type=self.group and 'info' or 'error') + if self.group and not self.groupname: + target_url = '%s/%s' % (self.context.absolute_url(), '@@usergroup-groupprefs') + self.request.response.redirect(target_url) + return '' + + return self.index() diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_groupmembership.pt b/Products/CMFPlone/controlpanel/browser/usergroups_groupmembership.pt new file mode 100644 index 0000000000..c1cb31305f --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_groupmembership.pt @@ -0,0 +1,345 @@ + + + + + + +
+ +
+ + +
+   +
+
+ +
+ Portal status message +
+ +
+ + +

Group Members

+ +
+ + Up to Groups Overview + + +

No group was specified.

+ +

+ Find a group here +

+
+
+ + +

+ Members of the Groupname group +

+ +
+ + Up to Groups Overview + + +

+ You can add or remove groups and users from this particular group here. Note that this + doesn't actually delete the group or user, it is only removed from this group. +

+ +
+

Current group members

+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + User nameE-mail Address
+ + + + + + () + + + + + Full Name + + () + + + + + + +
+ + +

There is no group or user attached to this group.

+ + + + + +

Search for new group members

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Quick search: + + + + +
+ + Group/User nameE-mail Address
+ + + + + + Full Name + + () + + + + + + + + () + + + + + + +
No matches + Enter a group or user name to search for. + + Enter a group or user name to search for or click 'Show All'. +
+ + + + + +
+ + + + + + + + + + +
+ + +
+
+ +
+ + + diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_groupmembership.py b/Products/CMFPlone/controlpanel/browser/usergroups_groupmembership.py new file mode 100644 index 0000000000..2f7eb5f0f7 --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_groupmembership.py @@ -0,0 +1,89 @@ +from Products.CMFPlone import PloneMessageFactory as _ +from zExceptions import Forbidden +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.controlpanel.browser.usergroups import \ + UsersGroupsControlPanelView +from Products.CMFPlone.utils import normalizeString + + +class GroupMembershipControlPanel(UsersGroupsControlPanelView): + + def update(self): + self.groupname = getattr(self.request, 'groupname') + self.gtool = getToolByName(self, 'portal_groups') + self.mtool = getToolByName(self, 'portal_membership') + self.group = self.gtool.getGroupById(self.groupname) + self.grouptitle = self.group.getGroupTitleOrName() or self.groupname + + self.request.set('grouproles', self.group.getRoles() if self.group else []) + self.canAddUsers = True + if 'Manager' in self.request.get('grouproles') and not self.is_zope_manager: + self.canAddUsers = False + + self.groupquery = self.makeQuery(groupname=self.groupname) + self.groupkeyquery = self.makeQuery(key=self.groupname) + + form = self.request.form + submitted = form.get('form.submitted', False) + + self.searchResults = [] + self.searchString = '' + self.newSearch = False + + if submitted: + # add/delete before we search so we don't show stale results + toAdd = form.get('add', []) + if toAdd: + if not self.canAddUsers: + raise Forbidden + + for u in toAdd: + self.gtool.addPrincipalToGroup(u, self.groupname, self.request) + self.context.plone_utils.addPortalMessage(_(u'Changes made.')) + + toDelete = form.get('delete', []) + if toDelete: + for u in toDelete: + self.gtool.removePrincipalFromGroup(u, self.groupname, self.request) + self.context.plone_utils.addPortalMessage(_(u'Changes made.')) + + search = form.get('form.button.Search', None) is not None + edit = form.get('form.button.Edit', None) is not None and toDelete + add = form.get('form.button.Add', None) is not None and toAdd + findAll = form.get('form.button.FindAll', None) is not None and \ + not self.many_users + # The search string should be cleared when one of the + # non-search buttons has been clicked. + if findAll or edit or add: + form['searchstring'] = '' + self.searchString = form.get('searchstring', '') + if findAll or bool(self.searchString): + self.searchResults = self.getPotentialMembers(self.searchString) + + if search or findAll: + self.newSearch = True + + self.groupMembers = self.getMembers() + + def __call__(self): + self.update() + return self.index() + + def isGroup(self, itemName): + return self.gtool.isGroup(itemName) + + def getMembers(self): + searchResults = self.gtool.getGroupMembers(self.groupname) + + groupResults = [self.gtool.getGroupById(m) for m in searchResults] + groupResults.sort(key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())) + + userResults = [self.mtool.getMemberById(m) for m in searchResults] + userResults.sort(key=lambda x: x is not None and x.getProperty('fullname') is not None and normalizeString(x.getProperty('fullname')) or '') + + mergedResults = groupResults + userResults + return filter(None, mergedResults) + + def getPotentialMembers(self, searchString): + ignoredUsersGroups = [x.id for x in self.getMembers() + [self.group,] if x is not None] + return self.membershipSearch(searchString, ignore=ignoredUsersGroups) diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_groupsoverview.pt b/Products/CMFPlone/controlpanel/browser/usergroups_groupsoverview.pt new file mode 100644 index 0000000000..8ce1210b2e --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_groupsoverview.pt @@ -0,0 +1,281 @@ + + + + + + +
+ +
+ +
 
+
+ +
+ Portal status message +
+ +
+ + Site Setup + +

Groups Overview

+ +
+ +

+ + Groups are logical collections of users, such as + departments and business units. Groups are not directly + related to permissions on a global level, you normally + use Roles for that - and let certain Groups have a + particular role. + + + The symbol + + indicates a role inherited from membership in another group. + +

+ +

+ Note: Some or all of your PAS groups + source plugins do not allow listing of groups, so you + may not see the groups defined by those plugins unless + doing a specific search. +

+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Group Search + + + + + + + + +
+ Group Name + + Roles + + Remove Group +
+ Role +
+ + + +   + () + + + + + + + + + +
+ No matches +
+ + + +
+ + + + + + + + + +
+
+
+ +
+ + + diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_groupsoverview.py b/Products/CMFPlone/controlpanel/browser/usergroups_groupsoverview.py new file mode 100644 index 0000000000..6deaedf24c --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_groupsoverview.py @@ -0,0 +1,144 @@ +from itertools import chain +from Acquisition import aq_inner +from Products.CMFPlone.controlpanel.browser.usergroups import \ + UsersGroupsControlPanelView +from Products.PluggableAuthService.interfaces.plugins import IRolesPlugin +from plone.protect import CheckAuthenticator +from zope.component import getMultiAdapter +from zExceptions import Forbidden +from Products.CMFPlone import PloneMessageFactory as _ +from Products.CMFCore.utils import getToolByName + + +class GroupsOverviewControlPanel(UsersGroupsControlPanelView): + + def __call__(self): + form = self.request.form + submitted = form.get('form.submitted', False) + search = form.get('form.button.Search', None) is not None + findAll = form.get('form.button.FindAll', None) is not None + self.searchString = not findAll and form.get('searchstring', '') or '' + self.searchResults = [] + self.newSearch = False + + if search or findAll: + self.newSearch = True + + if submitted: + if form.get('form.button.Modify', None) is not None: + self.manageGroup([group[len('group_'):] for group in self.request.keys() if group.startswith('group_')], + form.get('delete', [])) + + # Only search for all ('') if the many_users flag is not set. + if not(self.many_groups) or bool(self.searchString): + self.searchResults = self.doSearch(self.searchString) + + return self.index() + + def doSearch(self, searchString): + """ Search for a group by id or title""" + acl = getToolByName(self, 'acl_users') + rolemakers = acl.plugins.listPlugins(IRolesPlugin) + + searchView = getMultiAdapter((aq_inner(self.context), self.request), name='pas_search') + + # First, search for inherited roles assigned to each group. + # We push this in the request so that IRoles plugins are told provide + # the roles inherited from the groups to which the principal belongs. + self.request.set('__ignore_group_roles__', False) + self.request.set('__ignore_direct_roles__', True) + inheritance_enabled_groups = searchView.merge(chain(*[searchView.searchGroups(**{field: searchString}) for field in ['id', 'title']]), 'id') + allInheritedRoles = {} + for group_info in inheritance_enabled_groups: + groupId = group_info['id'] + group = acl.getGroupById(groupId) + group_info['title'] = group.getProperty('title', group_info['title']) + allAssignedRoles = [] + for rolemaker_id, rolemaker in rolemakers: + # getRolesForPrincipal can return None + roles = rolemaker.getRolesForPrincipal(group) or () + allAssignedRoles.extend(roles) + allInheritedRoles[groupId] = allAssignedRoles + + # Now, search for all roles explicitly assigned to each group. + # We push this in the request so that IRoles plugins don't provide + # the roles inherited from the groups to which the principal belongs. + self.request.set('__ignore_group_roles__', True) + self.request.set('__ignore_direct_roles__', False) + explicit_groups = searchView.merge(chain(*[searchView.searchGroups(**{field: searchString}) for field in ['id', 'title']]), 'id') + + # Tack on some extra data, including whether each role is explicitly + # assigned ('explicit'), inherited ('inherited'), or not assigned at all (None). + results = [] + for group_info in explicit_groups: + groupId = group_info['id'] + group = acl.getGroupById(groupId) + group_info['title'] = group.getProperty('title', group_info['title']) + + explicitlyAssignedRoles = [] + for rolemaker_id, rolemaker in rolemakers: + # getRolesForPrincipal can return None + roles = rolemaker.getRolesForPrincipal(group) or () + explicitlyAssignedRoles.extend(roles) + + roleList = {} + for role in self.portal_roles: + canAssign = group.canAssignRole(role) + if role == 'Manager' and not self.is_zope_manager: + canAssign = False + roleList[role]={'canAssign': canAssign, + 'explicit': role in explicitlyAssignedRoles, + 'inherited': role in allInheritedRoles.get(groupId, [])} + + canDelete = group.canDelete() + if ('Manager' in explicitlyAssignedRoles or + 'Manager' in allInheritedRoles.get(groupId, [])): + if not self.is_zope_manager: + canDelete = False + + group_info['roles'] = roleList + group_info['can_delete'] = canDelete + results.append(group_info) + # Sort the groups by title + sortedResults = searchView.sort(results, 'title') + + # Reset the request variable, just in case. + self.request.set('__ignore_group_roles__', False) + return sortedResults + + def manageGroup(self, groups=None, delete=None): + if groups is None: + groups = [] + if delete is None: + delete = [] + CheckAuthenticator(self.request) + context = aq_inner(self.context) + + groupstool=context.portal_groups + utils = getToolByName(context, 'plone_utils') + groupstool = getToolByName(context, 'portal_groups') + + message = _(u'No changes made.') + + for group in groups: + roles=[r for r in self.request.form['group_' + group] if r] + group_obj = groupstool.getGroupById(group) + current_roles = group_obj.getRoles() + if not self.is_zope_manager: + # don't allow adding or removing the Manager role + if ('Manager' in roles) != ('Manager' in current_roles): + raise Forbidden + + groupstool.editGroup(group, roles=roles, groups=()) + message = _(u'Changes saved.') + + if delete: + for group_id in delete: + group = groupstool.getGroupById(group_id) + if 'Manager' in group.getRoles() and not self.is_zope_manager: + raise Forbidden + + groupstool.removeGroups(delete) + message=_(u'Group(s) deleted.') + + utils.addPortalMessage(message) diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.pt b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.pt new file mode 100644 index 0000000000..622140e50a --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.pt @@ -0,0 +1,225 @@ + + + + + + +
+ + + +
+ Portal status message +
+ + +
+ +
+ + + diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py new file mode 100644 index 0000000000..ddf57206f9 --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py @@ -0,0 +1,63 @@ +from Products.CMFPlone import PloneMessageFactory as _ +from zExceptions import Forbidden +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.controlpanel.browser.usergroups import \ + UsersGroupsControlPanelView +from Products.CMFPlone.utils import normalizeString + + +class UserMembershipControlPanel(UsersGroupsControlPanelView): + + def update(self): + self.userid = getattr(self.request, 'userid') + self.gtool = getToolByName(self, 'portal_groups') + self.mtool = getToolByName(self, 'portal_membership') + self.member = self.mtool.getMemberById(self.userid) + + form = self.request.form + + self.searchResults = [] + self.searchString = '' + self.newSearch = False + + if form.get('form.submitted', False): + delete = form.get('delete', []) + if delete: + for groupname in delete: + self.gtool.removePrincipalFromGroup(self.userid, groupname, self.request) + self.context.plone_utils.addPortalMessage(_(u'Changes made.')) + + add = form.get('add', []) + if add: + for groupname in add: + group = self.gtool.getGroupById(groupname) + if 'Manager' in group.getRoles() and not self.is_zope_manager: + raise Forbidden + + self.gtool.addPrincipalToGroup(self.userid, groupname, self.request) + self.context.plone_utils.addPortalMessage(_(u'Changes made.')) + + search = form.get('form.button.Search', None) is not None + findAll = form.get('form.button.FindAll', None) is not None and not self.many_groups + self.searchString = not findAll and form.get('searchstring', '') or '' + + if findAll or not self.many_groups or self.searchString != '': + self.searchResults = self.getPotentialGroups(self.searchString) + + if search or findAll: + self.newSearch = True + + self.groups = self.getGroups() + + def __call__(self): + self.update() + return self.index() + + def getGroups(self): + groupResults = [self.gtool.getGroupById(m) for m in self.gtool.getGroupsForPrincipal(self.member)] + groupResults.sort(key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())) + return filter(None, groupResults) + + def getPotentialGroups(self, searchString): + ignoredGroups = [x.id for x in self.getGroups() if x is not None] + return self.membershipSearch(searchString, searchUsers=False, ignore=ignoredGroups) diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_usersoverview.pt b/Products/CMFPlone/controlpanel/browser/usergroups_usersoverview.pt new file mode 100644 index 0000000000..89a1a89c8c --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_usersoverview.pt @@ -0,0 +1,253 @@ + + + + + + +
+
+ +
 
+
+ +
+ Portal status message +
+ +
+ + +

Users Overview

+ +
+

+ Click the user's name to see and change the details of a + specific user. You can also add and remove users. +

+

+ Note that roles set here apply directly to a user. + The symbol + indicates a role inherited from membership in a group. +

+

+ Note + Some or all of your PAS user source + plugins do not allow listing of users, so you may not see + the users defined by those plugins unless doing a specific + search. +

+

+ + + +

+
+ + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User nameRolesReset PasswordRemove user
Role
+ + Full Name + login name + + + + + + + + + + + + + +
No matches + Enter a username to search for + + Enter a username to search for, or click 'Show All' +
+ +
+ + + + + + + +
+ +
+ + + + +
+
+
+ +
+ + + + diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_usersoverview.py b/Products/CMFPlone/controlpanel/browser/usergroups_usersoverview.py new file mode 100644 index 0000000000..20fd97eba4 --- /dev/null +++ b/Products/CMFPlone/controlpanel/browser/usergroups_usersoverview.py @@ -0,0 +1,234 @@ +import logging +from Acquisition import aq_inner +from zExceptions import Forbidden +from itertools import chain + +from Products.PluggableAuthService.interfaces.plugins import IRolesPlugin +from zope.component import getUtility +from plone.protect import CheckAuthenticator +from zope.component import getMultiAdapter +from Products.CMFCore.interfaces import ISiteRoot +from Products.CMFPlone import PloneMessageFactory as _ +from Products.CMFCore.utils import getToolByName + +from Products.CMFPlone.utils import normalizeString +from Products.CMFPlone.controlpanel.browser.usergroups import \ + UsersGroupsControlPanelView + +logger = logging.getLogger('Products.CMFPlone') + + +class UsersOverviewControlPanel(UsersGroupsControlPanelView): + + def __call__(self): + + form = self.request.form + submitted = form.get('form.submitted', False) + search = form.get('form.button.Search', None) is not None + findAll = form.get('form.button.FindAll', None) is not None + self.searchString = not findAll and form.get('searchstring', '') or '' + self.searchResults = [] + self.newSearch = False + + if search or findAll: + self.newSearch = True + + if submitted: + if form.get('form.button.Modify', None) is not None: + self.manageUser(form.get('users', None), + form.get('resetpassword', []), + form.get('delete', [])) + + # Only search for all ('') if the many_users flag is not set. + if not(self.many_users) or bool(self.searchString): + self.searchResults = self.doSearch(self.searchString) + + return self.index() + + def doSearch(self, searchString): + acl = getToolByName(self, 'acl_users') + rolemakers = acl.plugins.listPlugins(IRolesPlugin) + + mtool = getToolByName(self, 'portal_membership') + + searchView = getMultiAdapter(( + aq_inner(self.context), + self.request + ), name='pas_search') + + # First, search for all inherited roles assigned to each group. + # We push this in the request so that IRoles plugins are told provide + # the roles inherited from the groups to which the principal belongs. + self.request.set('__ignore_group_roles__', False) + self.request.set('__ignore_direct_roles__', True) + inheritance_enabled_users = searchView.merge( + chain(*[searchView.searchUsers(**{field: searchString}) for field in ['login', 'fullname', 'email']]), 'userid') + allInheritedRoles = {} + for user_info in inheritance_enabled_users: + userId = user_info['id'] + user = acl.getUserById(userId) + # play safe, though this should never happen + if user is None: + logger.warn('Skipped user without principal object: %s' % userId) + continue + allAssignedRoles = [] + for rolemaker_id, rolemaker in rolemakers: + allAssignedRoles.extend(rolemaker.getRolesForPrincipal(user)) + allInheritedRoles[userId] = allAssignedRoles + + # We push this in the request such IRoles plugins don't provide + # the roles from the groups the principal belongs. + self.request.set('__ignore_group_roles__', True) + self.request.set('__ignore_direct_roles__', False) + explicit_users = searchView.merge( + chain(*[searchView.searchUsers(**{field: searchString}) for field in ['login', 'fullname', 'email']]), 'userid' + ) + + # Tack on some extra data, including whether each role is explicitly + # assigned ('explicit'), inherited ('inherited'), or not assigned at + # all (None). + results = [] + for user_info in explicit_users: + userId = user_info['id'] + user = mtool.getMemberById(userId) + # play safe, though this should never happen + if user is None: + logger.warn('Skipped user without principal object: %s' % userId) + continue + explicitlyAssignedRoles = [] + for rolemaker_id, rolemaker in rolemakers: + explicitlyAssignedRoles.extend(rolemaker.getRolesForPrincipal(user)) + + roleList = {} + for role in self.portal_roles: + canAssign = user.canAssignRole(role) + if role == 'Manager' and not self.is_zope_manager: + canAssign = False + roleList[role]={'canAssign': canAssign, + 'explicit': role in explicitlyAssignedRoles, + 'inherited': role in allInheritedRoles[userId]} + + canDelete = user.canDelete() + canPasswordSet = user.canPasswordSet() + if roleList['Manager']['explicit'] or roleList['Manager']['inherited']: + if not self.is_zope_manager: + canDelete = False + canPasswordSet = False + + user_info['roles'] = roleList + user_info['fullname'] = user.getProperty('fullname', '') + user_info['email'] = user.getProperty('email', '') + user_info['can_delete'] = canDelete + user_info['can_set_email'] = user.canWriteProperty('email') + user_info['can_set_password'] = canPasswordSet + results.append(user_info) + + # Sort the users by fullname + results.sort(key=lambda x: x is not None and x['fullname'] is not None and normalizeString(x['fullname']) or '') + + # Reset the request variable, just in case. + self.request.set('__ignore_group_roles__', False) + return results + + def manageUser(self, users=[], resetpassword=[], delete=[]): + CheckAuthenticator(self.request) + + if users: + context = aq_inner(self.context) + acl_users = getToolByName(context, 'acl_users') + mtool = getToolByName(context, 'portal_membership') + regtool = getToolByName(context, 'portal_registration') + + utils = getToolByName(context, 'plone_utils') + + users_with_reset_passwords = [] + + for user in users: + # Don't bother if the user will be deleted anyway + if user.id in delete: + continue + + member = mtool.getMemberById(user.id) + current_roles = member.getRoles() + # If email address was changed, set the new one + if hasattr(user, 'email'): + # If the email field was disabled (ie: non-writeable), the + # property might not exist. + if user.email != member.getProperty('email'): + utils.setMemberProperties(member, REQUEST=context.REQUEST, email=user.email) + utils.addPortalMessage(_(u'Changes applied.')) + + # If reset password has been checked email user a new password + pw = None + if hasattr(user, 'resetpassword'): + if 'Manager' in current_roles and not self.is_zope_manager: + raise Forbidden + if not context.unrestrictedTraverse('@@overview-controlpanel').mailhost_warning(): + pw = regtool.generatePassword() + else: + utils.addPortalMessage(_(u'No mailhost defined. Unable to reset passwords.'), type='error') + + roles = user.get('roles', []) + if not self.is_zope_manager: + # don't allow adding or removing the Manager role + if ('Manager' in roles) != ('Manager' in current_roles): + raise Forbidden + + acl_users.userFolderEditUser(user.id, pw, roles, member.getDomains(), REQUEST=context.REQUEST) + if pw: + context.REQUEST.form['new_password'] = pw + regtool.mailPassword(user.id, context.REQUEST) + users_with_reset_passwords.append(user.id) + + if delete: + self.deleteMembers(delete) + if users_with_reset_passwords: + reset_passwords_message = _( + u"reset_passwords_msg", + default=u"The following users have been sent an e-mail with link to reset their password: ${user_ids}", + mapping={ + u"user_ids" : ', '.join(users_with_reset_passwords), + }, + ) + utils.addPortalMessage(reset_passwords_message) + utils.addPortalMessage(_(u'Changes applied.')) + + def deleteMembers(self, member_ids): + # this method exists to bypass the 'Manage Users' permission check + # in the CMF member tool's version + context = aq_inner(self.context) + mtool = getToolByName(self.context, 'portal_membership') + + # Delete members in acl_users. + acl_users = context.acl_users + if isinstance(member_ids, basestring): + member_ids = (member_ids,) + member_ids = list(member_ids) + for member_id in member_ids[:]: + member = mtool.getMemberById(member_id) + if member is None: + member_ids.remove(member_id) + else: + if not member.canDelete(): + raise Forbidden + if 'Manager' in member.getRoles() and not self.is_zope_manager: + raise Forbidden + try: + acl_users.userFolderDelUsers(member_ids) + except (AttributeError, NotImplementedError): + raise NotImplementedError('The underlying User Folder ' + 'doesn\'t support deleting members.') + + # Delete member data in portal_memberdata. + mdtool = getToolByName(context, 'portal_memberdata', None) + if mdtool is not None: + for member_id in member_ids: + mdtool.deleteMemberData(member_id) + + # Delete members' local roles. + mtool.deleteLocalRoles( + getUtility(ISiteRoot), + member_ids, + reindex=1, + recursive=1 + ) diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_usergroups_adapter.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_usergroups_adapter.py new file mode 100644 index 0000000000..1264f215e1 --- /dev/null +++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_usergroups_adapter.py @@ -0,0 +1,43 @@ +from plone.app.testing import setRoles +from Products.CMFPlone.interfaces import IUserGroupsSettingsSchema +from zope.component import getAdapter +from plone.app.testing import TEST_USER_ID +from plone.registry.interfaces import IRegistry +from zope.component import getUtility + +import unittest2 as unittest + +from Products.CMFPlone.testing import \ + PRODUCTS_CMFPLONE_INTEGRATION_TESTING + + +class UserGroupsControlPanelAdapterTest(unittest.TestCase): + + layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer['portal'] + self.request = self.layer['request'] + setRoles(self.portal, TEST_USER_ID, ['Manager']) + registry = getUtility(IRegistry) + self.usergroups_settings = registry.forInterface( + IUserGroupsSettingsSchema, prefix="plone") + + def test_adapter_lookup(self): + self.assertTrue( + getAdapter(self.portal, IUserGroupsSettingsSchema) + ) + + def test_many_groups(self): + getAdapter(self.portal, IUserGroupsSettingsSchema).set_many_groups(True) + self.assertEqual( + getAdapter(self.portal, IUserGroupsSettingsSchema).get_many_groups(), + True + ) + + def test_many_users(self): + getAdapter(self.portal, IUserGroupsSettingsSchema).set_many_users(True) + self.assertEqual( + getAdapter(self.portal, IUserGroupsSettingsSchema).get_many_users(), + True + ) diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_usergroups.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_usergroups.py new file mode 100644 index 0000000000..d3c8e5dcdd --- /dev/null +++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_usergroups.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD +from Products.CMFCore.utils import getToolByName +from plone.testing.z2 import Browser +from Products.CMFPlone.utils import normalizeString + +from Products.CMFPlone.testing import \ + PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING + +import unittest2 as unittest +import transaction + + +class UserGroupsControlPanelFunctionalTest(unittest.TestCase): + """Test that changes in the user groups control panel are actually + creating and changing users and groups. + """ + + layer = PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING + + def _generateGroups(self): + groupsTool = getToolByName(self.portal, 'portal_groups') + self.groups = [ + {'id': 'group1', 'title': "Group 1"}, + {'id': 'group2', 'title': "Group 2"}, + {'id': 'group3', 'title': "Group 3 accentué"} + ] + for group in self.groups: + groupsTool.addGroup(group['id'], [], [], title=group['title']) + + def _generateUsers(self): + self.members = [ + {'username': 'DIispfuF', 'fullname': 'Kevin Hughes', 'email': 'DIispfuF@example.com'}, + {'username': 'enTHXigm', 'fullname': 'Richard Ramirez', 'email': 'enTHXigm@example.com'}, + {'username': 'q7UsYcrT', 'fullname': 'Kyle Brown', 'email': 'q7UsYcrT@example.com'}, + {'username': 'j5g0xPmr', 'fullname': 'Julian Green', 'email': 'j5g0xPmr@example.com'}, + {'username': 'o6Sx4It3', 'fullname': 'Makayla Coleman', 'email': 'o6Sx4It3@example.com'}, + {'username': 'SLUhquYa', 'fullname': 'Sean Foster', 'email': 'SLUhquYa@example.com'}, + {'username': 'nHWl3Ita', 'fullname': 'Molly Martin', 'email': 'nHWl3Ita@example.com'}, + {'username': 'xdkpCKmX', 'fullname': 'Jordan Thompson', 'email': 'xdkpCKmX@example.com'}, + {'username': 'p8H6CicB', 'fullname': 'Tyler Rivera', 'email': 'p8H6CicB@example.com'}, + {'username': 'T6vdBXbD', 'fullname': 'Megan Murphy', 'email': 'T6vdBXbD@example.com'}, + {'username': 'DohPmgIa', 'fullname': 'Gracie Diaz', 'email': 'DohPmgIa@example.com'}, + {'username': 'CqHWi65B', 'fullname': 'Rachel Morgan', 'email': 'CqHWi65B@example.com'}, + {'username': 'uHFQ7qk4', 'fullname': 'Maya Price', 'email': 'uHFQ7qk4@example.com'}, + {'username': 'BlXLQh7r', 'fullname': 'Blake Jenkins', 'email': 'BlXLQh7r@example.com'}, + {'username': 'FCrWUiSY', 'fullname': 'Owen Ramirez', 'email': 'FCrWUiSY@example.com'}, + {'username': 'bX3PqgHK', 'fullname': 'Owen Cook', 'email': 'bX3PqgHK@example.com'}, + {'username': 'sD35vVl0', 'fullname': 'Jayden Hill', 'email': 'sD35vVl0@example.com'}, + {'username': 'mfOcjXAG', 'fullname': 'Joseph Ramirez', 'email': 'mfOcjXAG@example.com'}, + {'username': 'GAJtdYbM', 'fullname': 'Nathan Young', 'email': 'GAJtdYbM@example.com'}, + {'username': 'E1OWG6bv', 'fullname': 'Kaitlyn Hernandez', 'email': 'E1OWG6bv@example.com'}, + {'username': 'BqOX2sCm', 'fullname': 'Faith Price', 'email': 'BqOX2sCm@example.com'}, + {'username': 'tyOxRnml', 'fullname': 'Sofia Williams', 'email': '5yOxRjtl@example.com'}, + {'username': 'fVcumDNl', 'fullname': 'David Sanders', 'email': 'fVcumDNl@example.com'}, + {'username': 'Ge1hqdEI', 'fullname': 'Jack Simmons', 'email': 'Ge1hqdEI@example.com'}, + {'username': 'o2CqT7kG', 'fullname': 'Cole Howard', 'email': 'o2CqT7kG@example.com'}, + {'username': 'mpGtfNl6', 'fullname': 'Rachel Miller', 'email': 'mpGtfNl6@example.com'}, + {'username': 'RGrpWiBg', 'fullname': 'Henry Patterson', 'email': 'RGrpWiBg@example.com'}, + {'username': 'Bufmi0YS', 'fullname': 'Avery Cooper', 'email': 'Bufmi0YS@example.com'}, + {'username': 'J7NvbjYd', 'fullname': 'Sydney Bennett', 'email': 'J7NvbjYd@example.com'}, + {'username': 'u5Xem8U1', 'fullname': 'Daniel Johnson', 'email': 'u5Xem8U1@example.com'}, + {'username': 'TWrMCLIo', 'fullname': 'Autumn Brooks', 'email': '0VrMCLIo@example.com'}, + {'username': 'FElYwiIr', 'fullname': 'Alexandra Nelson', 'email': 'FElYwiIr@example.com'}, + {'username': 'teK6pkhc', 'fullname': 'Brian Simmons', 'email': '0eK6pkhc@example.com'}, + {'username': 'RwAO2YPa', 'fullname': 'Kevin Hughes', 'email': 'gracie@example.com'}, + {'username': 'nlBMw26i', 'fullname': 'Sydney Evans', 'email': 'nlBMw26i@example.com'}, + {'username': 'Ahr3EiRC', 'fullname': 'Emma Brown', 'email': 'Ahr3EiRC@example.com'}, + {'username': 'NhuU0Y5x', 'fullname': 'Lauren Martin', 'email': 'NhuU0Y5x@example.com'}, + {'username': 'j2R3mKQg', 'fullname': 'Isabelle Russell', 'email': 'j2R3mKQg@example.com'}, + {'username': 'qOmK0iCN', 'fullname': 'Anna Baker', 'email': 'qOmK0iCN@example.com'}, + {'username': 'uQbVOgo7', 'fullname': 'Brady Watson', 'email': 'uQbVOgo7@example.com'}, + {'username': 'oLDCaQfW', 'fullname': 'Kaitlyn Robinson', 'email': 'oLDCaQfW@example.com'}, + {'username': 'osYHeFD1', 'fullname': 'Riley Richardson', 'email': 'osYHeFD1@example.com'}, + {'username': 'i4pHduDY', 'fullname': 'Kayla Sanders', 'email': 'i4pHduDY@example.com'}, + {'username': 'BvyX6qF3', 'fullname': 'Sara Richardson', 'email': 'BvyX6qF3@example.com'}, + {'username': 'a3EpwDYj', 'fullname': 'Trinity Gonzales', 'email': 'a3EpwDYj@example.com'}, + {'username': 'JDMseWdt', 'fullname': 'Madeline Garcia', 'email': 'JDMseWdt@example.com'}, + {'username': 'lPCYBvoi', 'fullname': 'Brian Gray', 'email': 'lPCYBvoi@example.com'}, + {'username': 'AByCsRQ3', 'fullname': 'Victoria Perez', 'email': 'AByCsRQ3@example.com'}, + {'username': 'CH7uVlNy', 'fullname': 'Charles Rodriguez', 'email': '5H7uVlNy@example.com'}, + {'username': 'XYsmd7ux', 'fullname': 'Abigail Simmons', 'email': 'XYsmd7ux@example.com'}, + {'username': 'DfaA1wqC3', 'fullname': 'Émilie Richard', 'email': 'DfaA1wqC3@example.com'}, + ] + rtool = getToolByName(self.portal, 'portal_registration') + for member in self.members: + rtool.addMember(member['username'], 'somepassword', properties=member) + + def setUp(self): + self.app = self.layer['app'] + self.portal = self.layer['portal'] + self.portal_url = self.portal.absolute_url() + self.usergroups_url = "%s/@@usergroup-userprefs" % self.portal_url + self.groups_url = "%s/@@usergroup-groupprefs" % self.portal_url + self._generateGroups() + self._generateUsers() + transaction.commit() + + self.browser = Browser(self.app) + self.browser.handleErrors = False + self.browser.addHeader( + 'Authorization', + 'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,) + ) + + def test_usergroups_control_panel_link(self): + self.browser.open( + "%s/plone_control_panel" % self.portal_url) + self.browser.getLink('Users and Groups').click() + self.assertEqual( + self.browser.url, + self.usergroups_url + ) + + def test_usergroups_groups_link(self): + self.browser.open(self.usergroups_url) + self.browser.getLink('Groups').click() + self.assertEqual( + self.browser.url, + "%s/@@usergroup-groupprefs" % self.portal_url + ) + + def test_usergroups_settings_link(self): + self.browser.open(self.usergroups_url) + self.browser.getLink('Settings').click() + self.assertEqual( + self.browser.url, + "%s/@@usergroup-controlpanel" % self.portal_url + ) + + def test_usergroups_member_registration_link(self): + self.browser.open(self.usergroups_url) + self.browser.getLink('Member Registration').click() + self.assertEqual( + self.browser.url, + "%s/@@member-registration" % self.portal_url + ) + + def test_user_search_by_name(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'Richard' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Richard Ramirez', self.browser.contents) + self.assertIn('Sara Richardson', self.browser.contents) + self.assertIn('Émilie Richard', self.browser.contents) + + def test_user_search_by_name_accent(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'Émilie' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Émilie Richard', self.browser.contents) + + def test_user_search_by_id(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'TWrMCLIo' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Autumn Brooks', self.browser.contents) + + def test_user_search_by_mail(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'DohPmgIa@' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Gracie Diaz', self.browser.contents) + + def test_user_show_all(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='form.button.FindAll').click() + + # Check that first 10 members (sorted by fullname) are shown. + for member in sorted( + self.members, key=lambda k: normalizeString(k['fullname']) + )[:10]: + self.assertIn(member['fullname'], self.browser.contents) + + def test_user_show_all_with_search_term(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'no-user' + self.browser.getControl(name='form.button.FindAll').click() + + # Check that all members is shown and search term is ignored + self.assertIn('Avery Cooper', self.browser.contents) + + def test_user_add_new_link(self): + self.browser.open(self.usergroups_url) + self.browser.getLink(id='add-user').click() + self.assertEqual( + self.browser.url, + "%s/@@new-user" % self.portal_url + ) + + def test_user_modify_roles(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'TWrMCLIo' + self.browser.getControl(name='form.button.Search').click() + + # Check that contributor role is not enabled and enable it + self.assertFalse(self.browser.getControl( + name='users.roles:list:records' + ).getControl(value='Contributor').selected) + self.browser.getControl( + name='users.roles:list:records' + ).getControl(value='Contributor').selected = True + self.browser.getControl(name='form.button.Modify').click() + + # Check that contributor role is now enabled for this user + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'TWrMCLIo' + self.browser.getControl(name='form.button.Search').click() + self.assertTrue(self.browser.getControl( + name='users.roles:list:records' + ).getControl(value='Contributor').selected) + + def test_user_delete(self): + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = 'TWrMCLIo' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Autumn Brooks', self.browser.contents) + + # Delete user + self.browser.getControl(name='delete:list').getControl( + value='TWrMCLIo').selected = True + self.browser.getControl(name='form.button.Modify').click() + + # Check that user does not exist anymore + self.browser.getControl(name='searchstring').value = 'TWrMCLIo' + self.browser.getControl(name='form.button.Search').click() + self.assertNotIn('Autumn Brooks', self.browser.contents) + + def test_groups_search_by_id(self): + self.browser.open(self.groups_url) + self.browser.getControl(name='searchstring').value = 'group1' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Group 1', self.browser.contents) + + def test_groups_search_by_name(self): + self.browser.open(self.groups_url) + self.browser.getControl(name='searchstring').value = 'Group 3 accentué' + self.browser.getControl(name='form.button.Search').click() + self.assertIn('Group 3 accentué', self.browser.contents) + + def test_groups_modify_roles(self): + self.browser.open(self.groups_url) + self.browser.getControl(name='searchstring').value = 'group1' + + # Check that role is not selected yet and then select it and apply it. + self.assertFalse(self.browser.getControl( + name='group_group1:list', index=1 + ).getControl(value='Contributor').selected) + self.browser.getControl( + name='group_group1:list', index=1 + ).getControl(value='Contributor').selected = True + self.browser.getControl('Apply Changes').click() + + # Check that role is now selected + self.browser.getControl( + name='group_group1:list', index=1 + ).getControl(value='Contributor').selected + + def test_groups_delete_group(self): + self.browser.open(self.groups_url) + self.browser.getControl(name='searchstring').value = 'group1' + + # Delete a group + self.browser.getControl( + name='delete:list' + ).getControl(value='group1').selected = True + self.browser.getControl(name='form.button.Modify').click() + + # Check that group doesn't exist anymore + self.browser.getControl(name='searchstring').value = 'group1' + self.assertNotIn('Group 1', self.browser.contents) + + def test_groups_show_all(self): + self.browser.open(self.groups_url) + self.browser.getControl(name='form.button.FindAll').click() + + for group in self.groups: + self.assertIn(group['title'], self.browser.contents) + + def test_group_add_users(self): + self.browser.open(self.groups_url) + self.browser.getLink('Group 1 (group1)').click() + self.assertIn( + 'There is no group or user attached to this group.', + self.browser.contents + ) + + # Add user (Autumn Brooks) to selected group (Group 1) + self.browser.getControl(name='searchstring').value = 'TWrMCLIo' + self.browser.getControl(name='form.button.Search').click() + self.browser.getControl(name='add:list').getControl( + value='TWrMCLIo').selected = True + + # Check that user is now part of the group + self.browser.getControl( + 'Add selected groups and users to this group').click() + self.assertIn('Autumn Brooks', self.browser.contents) + + def test_group_add_group(self): + self.browser.open(self.groups_url) + self.browser.getLink('Group 1 (group1)').click() + self.assertIn( + 'There is no group or user attached to this group.', + self.browser.contents + ) + + # Add group2 to selected group 1 + self.browser.getControl(name='searchstring').value = 'group2' + self.browser.getControl(name='form.button.Search').click() + self.browser.getControl(name='add:list').getControl( + value='group2').selected = True + + # Check that group is now part of the group + self.browser.getControl( + 'Add selected groups and users to this group').click() + self.assertIn('Group 2', self.browser.contents) + + def test_usergroups_settings_many_users(self): + self.browser.open("%s/@@usergroup-controlpanel" % self.portal_url) + self.browser.getControl( + name='form.widgets.many_users:list' + ).controls[0].selected = True + self.browser.getControl('Apply').click() + + # Check that show all button for users is no longer available + self.browser.open(self.usergroups_url) + self.assertNotIn('Show all', self.browser.contents) + + # Check that empty search does not trigger show all + self.browser.open(self.usergroups_url) + self.browser.getControl(name='searchstring').value = '' + + def test_usergroups_settings_many_groups(self): + self.browser.open("%s/@@usergroup-controlpanel" % self.portal_url) + self.browser.getControl( + name='form.widgets.many_groups:list' + ).controls[0].selected = True + self.browser.getControl('Apply').click() + + # Check that show all button for groups is no longer available + self.browser.open(self.groups_url) + self.assertNotIn('Show all', self.browser.contents) + self.assertNotIn('DIispfuF', self.browser.contents) diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py index 0f0abb3262..aecf6da3f3 100644 --- a/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py +++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_overview.py @@ -32,7 +32,7 @@ def setUp(self): self.request = self.layer['request'] setRoles(self.portal, TEST_USER_ID, ['Manager']) - @mock.patch('plone.app.controlpanel.overview.getUtility', + @mock.patch('Products.CMFPlone.controlpanel.browser.overview.getUtility', new=mock_getUtility1) def test_timezone_warning__noreg(self): # If no registry key is available, return True @@ -43,7 +43,7 @@ def test_timezone_warning__noreg(self): view = self.portal.restrictedTraverse('@@overview-controlpanel') self.assertTrue(view.timezone_warning()) - @mock.patch('plone.app.controlpanel.overview.getUtility', + @mock.patch('Products.CMFPlone.controlpanel.browser.overview.getUtility', new=mock_getUtility2) def test_timezone_warning__emptyreg(self): # If registry key value is empty, return True @@ -53,7 +53,7 @@ def test_timezone_warning__emptyreg(self): view = self.portal.restrictedTraverse('@@overview-controlpanel') self.assertTrue(view.timezone_warning()) - @mock.patch('plone.app.controlpanel.overview.getUtility', + @mock.patch('Products.CMFPlone.controlpanel.browser.overview.getUtility', new=mock_getUtility3) def test_timezone_warning__set(self): # If new plone.portal_timezone is set, return False diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_usergroups.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_usergroups.py new file mode 100644 index 0000000000..03b1e22712 --- /dev/null +++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_usergroups.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from Products.CMFPlone.interfaces import IUserGroupsSettingsSchema +import unittest2 as unittest + +from zope.component import getMultiAdapter +from zope.component import getUtility +from plone.registry.interfaces import IRegistry + +from Products.CMFCore.utils import getToolByName + +from Products.CMFPlone.testing import \ + PRODUCTS_CMFPLONE_INTEGRATION_TESTING + + +class TypesRegistryIntegrationTest(unittest.TestCase): + """Tests that the types settings are stored as plone.app.registry + settings. + """ + + layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer['portal'] + self.request = self.layer['request'] + registry = getUtility(IRegistry) + self.settings = registry.forInterface( + IUserGroupsSettingsSchema, prefix="plone") + + def test_usergroups_controlpanel_view(self): + view = getMultiAdapter((self.portal, self.portal.REQUEST), + name="usergroup-controlpanel") + view = view.__of__(self.portal) + self.assertTrue(view()) + + def test_usergroups_in_controlpanel(self): + self.controlpanel = getToolByName(self.portal, "portal_controlpanel") + self.assertTrue('UsersGroups' in [ + a.getAction(self)['id'] + for a in self.controlpanel.listActions() + ]) + + def test_many_groups_setting(self): + self.assertTrue(hasattr(self.settings, 'many_groups')) + + def test_many_users_setting(self): + self.assertTrue(hasattr(self.settings, 'many_users')) diff --git a/Products/CMFPlone/interfaces/__init__.py b/Products/CMFPlone/interfaces/__init__.py index 029932119f..3eeebf2990 100644 --- a/Products/CMFPlone/interfaces/__init__.py +++ b/Products/CMFPlone/interfaces/__init__.py @@ -22,6 +22,7 @@ from controlpanel import ITinyMCESchema from controlpanel import ITinyMCEToolbarSchema from controlpanel import ITypesSchema +from controlpanel import IUserGroupsSettingsSchema from events import IConfigurationChangedEvent from events import IReorderedEvent from events import ISiteManagerCreatedEvent diff --git a/Products/CMFPlone/interfaces/controlpanel.py b/Products/CMFPlone/interfaces/controlpanel.py index 1c1a1a836f..068ff04dd2 100644 --- a/Products/CMFPlone/interfaces/controlpanel.py +++ b/Products/CMFPlone/interfaces/controlpanel.py @@ -1035,3 +1035,32 @@ class IMarkupSchema(Interface): vocabulary="plone.app.vocabularies.AllowableContentTypes" ) ) + + +class IUserGroupsSettingsSchema(Interface): + + many_groups = schema.Bool( + title=_(u'Many groups?'), + description=_( + u"Determines if your Plone is optimized " + u"for small or large sites. In environments with a " + u"lot of groups it can be very slow or impossible " + u"to build a list all groups. This option tunes the " + u"user interface and behaviour of Plone for this " + u"case by allowing you to search for groups instead " + u"of listing all of them."), + default=False + ) + + many_users = schema.Bool( + title=_(u'Many users?'), + description=_( + u"Determines if your Plone is optimized " + u"for small or large sites. In environments with a " + u"lot of users it can be very slow or impossible to " + u"build a list all users. This option tunes the user " + u"interface and behaviour of Plone for this case by " + u"allowing you to search for users instead of " + u"listing all of them."), + default=False + ) diff --git a/Products/CMFPlone/profiles/dependencies/registry.xml b/Products/CMFPlone/profiles/dependencies/registry.xml index b44ca4f648..771e113545 100644 --- a/Products/CMFPlone/profiles/dependencies/registry.xml +++ b/Products/CMFPlone/profiles/dependencies/registry.xml @@ -18,6 +18,8 @@ prefix="plone" /> + diff --git a/Products/CMFPlone/tests/robot/test_controlpanel_usergroups.robot b/Products/CMFPlone/tests/robot/test_controlpanel_usergroups.robot new file mode 100644 index 0000000000..4f8cd1f604 --- /dev/null +++ b/Products/CMFPlone/tests/robot/test_controlpanel_usergroups.robot @@ -0,0 +1,117 @@ +# ============================================================================ +# Tests for the Plone Usergroups Control Panel +# ============================================================================ +# +# $ bin/robot-server --reload-path src/Products.CMFPlone/Products/CMFPlone/ Products.CMFPlone.testing.PRODUCTS_CMFPLONE_ROBOT_TESTING +# +# $ bin/robot src/Products.CMFPlone/Products/CMFPlone/tests/robot/test_controlpanel_usergroups.robot +# +# ============================================================================ + +*** Settings ***************************************************************** + +Resource plone/app/robotframework/keywords.robot +Resource plone/app/robotframework/saucelabs.robot + +Library Remote ${PLONE_URL}/RobotRemote + +Resource keywords.robot + +Test Setup Open SauceLabs test browser +Test Teardown Run keywords Report test status Close all browsers + + +*** Test Cases *************************************************************** + +Scenario: Show all users in users control panel + Given a logged-in site administrator + and the usergroups control panel + When I click show all users + Then all users should be shown + +Scenario: Show all groups in groups control panel + Given a logged-in site administrator + and the usergroups control panel + When I go to Groups control panel + and I click show all groups + Then all groups should be shown + +Scenario: Create new group + Given a logged-in site administrator + and the usergroups control panel + When I go to Groups control panel + and I create new group + Then new group should show under all groups + +Scenario: Enable many groups and many users settings in usergroups control panel + Given a logged-in site administrator + and the usergroups control panel + When I go to Settings control panel + and enable many groups and many users settings + Then showing all users is disabled + and showing all groups is disabled + + +*** Keywords ***************************************************************** + +# --- GIVEN ------------------------------------------------------------------ + +a logged-in site administrator + Enable autologin as Site Administrator + +the usergroups control panel + Go to ${PLONE_URL}/@@usergroup-userprefs + + +# --- WHEN ------------------------------------------------------------------- + +I click show all users + Click button Show all + +I go to Groups control panel + Click link Groups + +I click show all groups + Click button Show all + +I create new group + Click button Add New Group + Input Text name=addname my-new-group + Input Text name=title:string My New Group + Input Text name=description:text This is my new group + Input Text name=email:string my-group@plone.org + Submit Form id=createGroup +# "Click button Save" does not work for modals. See https://stackoverflow.com/questions/17602334/element-is-not-currently-visible-and-so-may-not-be-interacted-with-but-another for details. + I click show all groups + Page should contain my-new-group + +I go to Settings control panel + Click link Settings + +enable many groups and many users settings + Select Checkbox name=form.widgets.many_groups:list + Select Checkbox name=form.widgets.many_users:list + Click button Apply + +# --- THEN ------------------------------------------------------------------- + +all users should be shown + Page should contain test-user + Page should contain admin + +all groups should be shown + Page should contain Administrators + Page should contain Authenticated Users (Virtual Group) (AuthenticatedUsers) + Page should contain Reviewers + Page should contain Site Administrators + +showing all users is disabled + Click link Users + Page should not contain Show all + +showing all groups is disabled + Click link Groups + Page should not contain Show all + +new group should show under all groups + Page should contain my-new-group