Skip to content

Commit

Permalink
Merge pull request #9 from ploneintranet/roster-changes
Browse files Browse the repository at this point in the history
Roster changes
  • Loading branch information
displacedaussie committed Jun 11, 2014
2 parents 7e58ef9 + faf282e commit b722dea
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 252 deletions.
8 changes: 0 additions & 8 deletions src/ploneintranet/workspace/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,4 @@
permission="cmf.ModifyPortalContent"
/>

<!-- We are also overriding collective.workspace.interfaces.IHasWorkspace -->
<browser:page
name="edit-roster"
for="ploneintranet.workspace.workspacefolder.IWorkspaceFolder"
class=".roster.EditRoster"
permission="cmf.ModifyPortalContent"
/>

</configure>
39 changes: 28 additions & 11 deletions src/ploneintranet/workspace/browser/roster-edit.pt
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@
title="Search for a user"
placeholder="Search for a user"
i18n:attributes="title; placeholder"
tal:attributes="value request/search_term|nothing"
class="searchField"
value=""
/>
<input type="submit"
id="edit-roster-search-button"
name="form.button.Search"
value="Search"
name="form.button.SearchUsers"
value="Search users"
class="searchButton allowMultiSubmit"
i18n:attributes="value label_search"
/>
Expand All @@ -54,31 +55,49 @@
<thead metal:define-macro="edit-roster-head" id="edit-roster-head">
<tr>
<th i18n:translate="label_name">Name</th>
<th class="nosort">Member</th>
<th class="nosort">Participant</th>
<th class="nosort">Workspace Admin</th>
</tr>
</thead>

<tbody metal:define-macro="edit-roster-settings" id="edit-roster-settings">
<tal:entries repeat="user users">
<tr tal:define="oddrow repeat/user/odd;
disabled user/disabled | python:False;
member user/member | python:False;"
is_admin user/admin | nothing;
is_member user/member | nothing"
tal:attributes="class python:oddrow and 'odd' or 'even'">
<td>
<span tal:replace="user/title" />
<span tal:replace="user/title" />
<input
type="hidden"
name="entries.id:records"
tal:attributes="value user/id"
/>
<input
type="hidden"
name="entries.type:records"
value="user"
/>
</td>
<td class="listingCheckbox">
<input class="noborder"
type="checkbox"
value="1"
name="entries.update:records"
tal:attributes="disabled python: disabled or None;
checked python: member and 'checked' or None"
name="entries.member:records"
tal:attributes="checked is_member;
disabled is_admin"
/>
<input type="hidden"
name="entries.member:records"
value="1"
tal:condition="is_admin" />
</td>
<td class="listingCheckbox">
<input class="noborder"
type="checkbox"
value="1"
name="entries.admin:records"
tal:attributes="checked is_admin;"
/>
</td>
</tr>
Expand All @@ -88,8 +107,6 @@
</div>

<input id="edit-roster-save-button" class="context allowMultiSubmit" type="submit" name="form.button.Save" value="Save" i18n:attributes="value label_save" />
<input class="standalone" type="submit" name="form.button.Cancel" value="Cancel" i18n:attributes="value label_cancel"/>

<input tal:replace="structure context/@@authenticator/authenticator" />

</form>
Expand Down
201 changes: 44 additions & 157 deletions src/ploneintranet/workspace/browser/roster.py
Original file line number Diff line number Diff line change
@@ -1,111 +1,68 @@
from itertools import chain
from plone import api
from plone.memoize.instance import memoize
from zope.i18n import translate
from plone.memoize.instance import memoize, clearafter
from zope.component import getMultiAdapter
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import safe_unicode
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

from zExceptions import Forbidden

from collective.workspace.interfaces import IWorkspace
from plone.protect import CheckAuthenticator, PostOnly
from Products.Five import BrowserView
from ploneintranet.workspace import MessageFactory as _
from collective.workspace.interfaces import IWorkspace


def merge_search_results(results, key):
"""Merge member search results.
class EditRoster(BrowserView):
"""Roster management page.
Based on PlonePAS.browser.search.PASSearchView.merge.
Based on the @@sharing tab from plone.app.workflow
"""
output = {}
for entry in results:
id = entry[key]
if id not in output:
output[id] = entry.copy()
else:
buf = entry.copy()
buf.update(output[id])
output[id] = buf

return output.values()


class EditRoster(BrowserView):

index = ViewPageTemplateFile('roster-edit.pt')

def __call__(self):
"""Perform the update and redirect if necessary, or render the page
"""
postback = self.handle_form()
if postback:
return self.index()
else:
url = '%s/team-roster' % self.context.absolute_url()
self.request.response.redirect(url)
self.handle_form()
return self.index()

def handle_form(self):
"""
We split this out so we can reuse this for ajax.
Will return a boolean if it was a post or not
"""
postback = True

form = self.request.form
submitted = form.get('form.submitted', False)
save_button = form.get('form.button.Save', None) is not None
cancel_button = form.get('form.button.Cancel', None) is not None
if submitted and save_button and not cancel_button:
if not self.request.get('REQUEST_METHOD', 'GET') == 'POST':
raise Forbidden

authenticator = self.context.restrictedTraverse('@@authenticator',
None)
if not authenticator.verify():
raise Forbidden

# Update settings for users and groups
if submitted and save_button:
CheckAuthenticator(self.request)
PostOnly(self.request)
entries = form.get('entries', [])
settings = []
for entry in entries:
settings.append(
dict(
id=entry['id'],
member=entry.get('update') == '1',
)
)
if settings:
self.update_users(settings)
# IStatusMessage(self.request).addStatusMessage(
# _(u"Changes saved."), type='info')

# Other buttons return to the sharing page
if cancel_button:
postback = False
self.update_users(entries)
api.portal.show_message(message=_(u'Roster updated.'),
request=self.request)

return postback

def update_users(self, settings):
@clearafter
def update_users(self, entries):
"""Update user properties on the roster """
ws = IWorkspace(self.context)
members = ws.members

for setting in settings:
for entry in entries:

id = setting['id']
is_member = setting['member']
id = entry['id']
is_member = bool(entry.get('member'))
is_admin = bool(entry.get('admin'))

# Existing members, no-longer selected
if id in members and not is_member:
# Existing members
if id in members:
member = members[id]
# Don't remove them if they're an Admin
if 'Admins' not in member['groups']:
if not is_member:
ws.membership_factory(ws, member).remove_from_team()
elif not is_admin:
ws.membership_factory(ws, member).groups -= {'Admins'}
else:
ws.membership_factory(ws, member).groups |= {'Admins'}

# New members
if id not in members and is_member:
ws.add_to_team(user=id)
elif id not in members and (is_member or is_admin):
groups = set()
if is_admin:
groups.add('Admins')
ws.add_to_team(user=id, groups=groups)

def users(self):
"""Get current users.
Expand All @@ -116,12 +73,17 @@ def users(self):
- title
"""
existing_users = self.existing_users()
user_results = self.user_search_results()
existing_user_ids = [x['id'] for x in existing_users]

current_users = existing_users + user_results
# Only add search results that are not already members
sharing = getMultiAdapter((self.context, self.request),
name='sharing')
search_results = sharing.user_search_results()
users = existing_users + [x for x in search_results
if x['id'] not in existing_user_ids]

current_users.sort(key=lambda x: safe_unicode(x["title"]))
return current_users
users.sort(key=lambda x: safe_unicode(x["title"]))
return users

@memoize
def existing_users(self):
Expand All @@ -135,83 +97,8 @@ def existing_users(self):
id=userid,
title=title,
member=True,
disabled='Admins' in details['groups'],
admin='Admins' in details['groups'],
)
)

return info

def _principal_search_results(self,
search_for_principal,
get_principal_by_id,
get_principal_title,
id_key):
"""Return search results for a query to add new users or groups.
Returns a list of dicts, as per role_settings().
Arguments:
search_for_principal -- a function that takes an IPASSearchView and
a search string. Uses the former to search for the latter and
returns the results.
get_principal_by_id -- a function that takes a user id and returns
the user of that id
get_principal_title -- a function that takes a user and a default
title and returns a human-readable title for the user. If it
can't think of anything good, returns the default title.
principal_type -- either 'user' or 'group', depending on what kind
of principals you want
id_key -- the key under which the principal id is stored in the
dicts returned from search_for_principal
"""
context = self.context

translated_message = translate(
_(u"Search for user or group"),
context=self.request)
search_term = self.request.form.get('search_term', None)
if not search_term or search_term == translated_message:
return []

existing_users = set([p['id'] for p in self.existing_users()])

info = []

hunter = getMultiAdapter((context, self.request), name='pas_search')
for principal_info in search_for_principal(hunter, search_term):
principal_id = principal_info[id_key]
if principal_id not in existing_users:
principal = get_principal_by_id(principal_id)
if principal is None:
continue

info.append(
dict(
id=principal_id,
title=get_principal_title(principal, principal_id),
disabled=False,
member=False,
))
return info

def user_search_results(self):
"""Return search results for a query to add new users.
Returns a list of dicts, as per role_settings().
"""

def search_for_principal(hunter, search_term):
return merge_search_results(
chain(*[hunter.searchUsers(**{field: search_term})
for field in ['name', 'fullname', 'email']]), 'userid')

def get_principal_by_id(user_id):
acl_users = getToolByName(self.context, 'acl_users')
return acl_users.getUserById(user_id)

def get_principal_title(user, default):
return user.getProperty('fullname') or user.getId() or default

return self._principal_search_results(
search_for_principal,
get_principal_by_id, get_principal_title, 'userid')
5 changes: 5 additions & 0 deletions src/ploneintranet/workspace/profiles/default/actions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<property name="available_expr">not:nocall:context/@@team-roster|nothing</property>
</object>

<!-- hide default collective.workspace roster tab -->
<object name="team-roster" meta_type="CMF Action" i18n:domain="plone">
<property name="visible">False</property>
</object>

<!-- Add settings tab on our workspace -->
<object name="ws_policies" meta_type="CMF Action" i18n:domain="plone">
<property name="title" i18n:translate="ws_policies_tab">Policies</property>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content" />
</action>
<action title="Roster" action_id="edit-roster"
category="object" condition_expr=""
url_expr="string:${object_url}/@@edit-roster" visible="True">
<permission value="collective.workspace: Manage roster" />
</action>
</object>
Loading

0 comments on commit b722dea

Please sign in to comment.