Skip to content

Commit ad3b174

Browse files
authored
Merge pull request #8194 from 4teamwork/ran/TI-2821/add-comment-memberships
Allow administrators to leave a note on Group Memberships
2 parents 285fde3 + 485882b commit ad3b174

32 files changed

Lines changed: 522 additions & 75 deletions

changes/TI-2821.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow admins to set a 'note' for every ogds membership. [ran]

docs/public/dev-manual/api/api_changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Breaking Changes
1212

1313
Other Changes
1414
^^^^^^^^^^^^^
15+
- ``@membership-notes``: Add endpoint to update a note on a membership.
1516

1617

1718
2025.8.0 (2025-08-22)

docs/public/dev-manual/api/users.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,42 @@ Mit dem ``@kub`` Endpoint können Kontakte aus dem KuB geholt werden. Der Endpoi
427427
"title": "",
428428
"urls": []
429429
}
430+
431+
432+
Vermerk Mitgliedschaften
433+
========================
434+
435+
Mit dem ``@membership-notes`` Endpoint können Vermerke bei den Mitgliedschaften editiert werden.
436+
Der Endpoint steht nur auf Stufe PloneSiteRoot zur Verfügung und erwartet als Pfad Parameter:
437+
438+
- ID des Users
439+
- ID der Gruppe
440+
- Vermerk als String
441+
442+
443+
**Beispiel-Request**:
444+
445+
.. sourcecode:: http
446+
447+
POST /@membership-notes HTTP/1.1
448+
Accept: application/json
449+
450+
{
451+
"userid": "hugo.boss",
452+
"groupid": "test_group",
453+
"note": "Example Note"
454+
}
455+
456+
**Beispiel-Response**:
457+
458+
459+
.. sourcecode:: http
460+
461+
HTTP/1.1 200 OK
462+
Content-Type: application/json
463+
464+
{
465+
"userid": "hugo.boss",
466+
"groupid": "test_group",
467+
"note": "Example Note"
468+
}

opengever/activity/sources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from opengever.ogds.base.sources import AllTeamsSource
44
from opengever.ogds.base.sources import AssignedUsersSource
55
from opengever.ogds.base.sources import BaseMultipleSourcesQuerySource
6-
from opengever.ogds.models.group import groups_users
6+
from opengever.ogds.models.group_membership import groups_users
77
from opengever.ogds.models.service import ogds_service
88
from opengever.ogds.models.user import User
99
from opengever.workspace import is_workspace_feature_enabled

opengever/api/configure.zcml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1881,4 +1881,13 @@
18811881
permission="zope.Public"
18821882
/>
18831883

1884+
<plone:service
1885+
method="POST"
1886+
name="@membership-notes"
1887+
for="Products.CMFCore.interfaces.ISiteRoot"
1888+
factory=".ogdsmembership_note.MembershipNotes"
1889+
permission="opengever.api.ManageGroups"
1890+
layer="opengever.base.interfaces.IOpengeverBaseLayer"
1891+
/>
1892+
18841893
</configure>

opengever/api/globalindex.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from opengever.globalindex.model.task import AVOID_DUPLICATES_STRATEGY_LOCAL
66
from opengever.globalindex.model.task import Task
77
from opengever.ogds.models.group import Group
8-
from opengever.ogds.models.group import groups_users
8+
from opengever.ogds.models.group_membership import groups_users
99
from opengever.ogds.models.team import Team
1010
from opengever.task.helper import task_type_value_helper
1111
from plone.restapi.interfaces import ISerializeToJson

opengever/api/group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from opengever.base.security import elevated_privileges
44
from opengever.base.utils import check_group_plugin_configuration
55
from opengever.ogds.models.group import Group
6-
from opengever.ogds.models.group import groups_users
6+
from opengever.ogds.models.group_membership import groups_users
77
from opengever.ogds.models.service import ogds_service
88
from opengever.ogds.models.user import User
99
from plone import api
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from opengever.base.model import create_session
2+
from opengever.ogds.models.group_membership import GroupMembership
3+
from plone.restapi.deserializer import json_body
4+
from plone.restapi.services import Service
5+
from sqlalchemy import and_
6+
from zExceptions import BadRequest
7+
8+
9+
class MembershipNotes(Service):
10+
11+
def reply(self):
12+
data = json_body(self.request) or {}
13+
14+
groupid = data.get('groupid')
15+
userid = data.get('userid')
16+
note = data.get('note', None)
17+
18+
if not groupid or not userid:
19+
raise BadRequest("Groupid and Userid are required.")
20+
21+
if isinstance(note, basestring):
22+
note = note.strip()
23+
normalized = note or None
24+
elif note is None:
25+
normalized = None
26+
else:
27+
raise BadRequest("Note must be a string or null.")
28+
29+
session = create_session()
30+
membership = (session.query(GroupMembership)
31+
.filter(and_(GroupMembership.groupid == groupid,
32+
GroupMembership.userid == userid))
33+
.one())
34+
35+
membership.note = normalized
36+
session.flush()
37+
38+
self.request.response.setStatus(200)
39+
return {
40+
"groupid": groupid,
41+
"userid": userid,
42+
"note": membership.note
43+
}

opengever/api/ogdsuserlisting.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from datetime import datetime
22
from opengever.api.ogdslistingbase import OGDSListingBaseService
33
from opengever.base.visible_users_and_groups_filter import visible_users_and_groups_filter
4-
from opengever.ogds.models.group import groups_users
4+
from opengever.ogds.models.group_membership import GroupMembership
5+
from opengever.ogds.models.group_membership import groups_users
56
from opengever.ogds.models.user import User
67
import re
78

@@ -35,6 +36,13 @@ class OGDSUserListingGet(OGDSListingBaseService):
3536
default_state_filter = tuple()
3637
pattern = re.compile(r"^(\d{4}-\d{2}-\d{2}) TO (\d{4}-\d{2}-\d{2})")
3738

39+
def reply(self):
40+
result = super(OGDSUserListingGet, self).reply()
41+
filters = self.request.form.get('filters', {})
42+
43+
self._augment_items_with_membership_note(result, filters)
44+
return result
45+
3846
def needs_join_with_groups_users(self, filters):
3947
return bool(filters.get('groupid', False))
4048

@@ -75,3 +83,31 @@ def extend_query_with_visible_users_and_groups_filter(self, query):
7583
visible_users_and_groups_filter.get_whitelisted_principals()))
7684

7785
return query
86+
87+
def _augment_items_with_membership_note(self, result, filters):
88+
groupid = filters.get('groupid', None)
89+
items = result.get('items') or []
90+
91+
userids = list({item.get('userid') for item in items})
92+
if not userids:
93+
return
94+
95+
rows = (
96+
GroupMembership.query
97+
.with_entities(GroupMembership.userid, GroupMembership.note)
98+
.filter(
99+
GroupMembership.groupid == groupid,
100+
GroupMembership.userid.in_(userids),
101+
GroupMembership.note.isnot(None),
102+
)
103+
.all()
104+
)
105+
notes_by_userid = dict(rows)
106+
107+
for item in items:
108+
uid = item.get('userid')
109+
if not uid:
110+
continue
111+
note = notes_by_userid.get(uid)
112+
if note:
113+
item['note'] = note

opengever/api/serializer.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
from opengever.ogds.base.actor import Actor
2121
from opengever.ogds.models.admin_unit import AdminUnit
2222
from opengever.ogds.models.group import Group
23-
from opengever.ogds.models.group import groups_users
23+
from opengever.ogds.models.group_membership import GroupMembership
24+
from opengever.ogds.models.group_membership import groups_users
2425
from opengever.ogds.models.org_unit import OrgUnit
2526
from opengever.ogds.models.team import Team
2627
from opengever.ogds.models.user import User
@@ -411,6 +412,26 @@ def add_additional_metadata(self, data):
411412
(team, self.request), ISerializeToJsonSummary)
412413
data['teams'].append(team_serializer())
413414

415+
groupids = [group.get('groupid') for group in data['groups']]
416+
417+
rows = (
418+
GroupMembership.query
419+
.with_entities(GroupMembership.groupid, GroupMembership.note)
420+
.filter(
421+
GroupMembership.userid == self.context.userid,
422+
GroupMembership.groupid.in_(groupids),
423+
GroupMembership.note.isnot(None),
424+
)
425+
.all()
426+
)
427+
428+
notes_by_groupid = dict(rows)
429+
430+
for group in data['groups']:
431+
note = notes_by_groupid.get(group.get("groupid"))
432+
if note:
433+
group["note"] = note
434+
414435
def __call__(self, *args, **kwargs):
415436
data = super(SerializeUserModelToJson, self).__call__(*args, **kwargs)
416437
if not is_administrator():

0 commit comments

Comments
 (0)