Skip to content

Commit d8a7f62

Browse files
committed
membership-request [inveniosoftware#855]: implement decision flow
- update role - addressed TODOs
1 parent 828ff70 commit d8a7f62

File tree

16 files changed

+558
-407
lines changed

16 files changed

+558
-407
lines changed

Diff for: invenio_communities/assets/semantic-ui/js/invenio_communities/api/membershipRequests/api.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
// Invenio-communities is free software; you can redistribute it and/or modify it
66
// under the terms of the MIT License; see LICENSE file for more details.
77

8-
import { CommunityLinksExtractor } from "../CommunityLinksExtractor";
98
import { http } from "react-invenio-forms";
109

10+
import { CommunityLinksExtractor } from "../CommunityLinksExtractor";
11+
import { bulkMembersSerializer } from "../serializers";
12+
1113
/**
1214
* API Client for community membership requests.
1315
*
@@ -21,6 +23,14 @@ export class CommunityMembershipRequestsApi {
2123
}
2224

2325
requestMembership = async (payload) => {
26+
// assigned rather than defiend for ease of passing as callback
2427
return await http.post(this.linksExtractor.url("membership_requests"), payload);
2528
};
29+
30+
updateRole = async (membershipRequest, role) => {
31+
// assigned rather than defiend for ease of passing as callback
32+
const memberSerialized = bulkMembersSerializer([membershipRequest]);
33+
const payload = { members: memberSerialized, role: role };
34+
return await http.put(this.linksExtractor.url("membership_requests"), payload);
35+
};
2636
}

Diff for: invenio_communities/assets/semantic-ui/js/invenio_communities/members/membership_requests/MembershipRequestsEmptyResults.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class MembershipRequestsEmptyResultsCmp extends Component {
1313
<Segment placeholder textAlign="center">
1414
<Header icon>
1515
<Icon name="search" />
16-
{i18next.t("No matching members found.")}
16+
{i18next.t("No matching membership requests found.")}
1717
</Header>
1818
{queryString && (
1919
<p>

Diff for: invenio_communities/assets/semantic-ui/js/invenio_communities/members/membership_requests/MembershipRequestsResultItem.js

+19-14
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import React, { Component } from "react";
1515
import { Image } from "react-invenio-forms";
1616
import { Grid, Item, Table } from "semantic-ui-react";
1717

18+
import { MembershipRequestsContext } from "../../api/membershipRequests/MembershipRequestsContextProvider";
1819
import { RoleDropdown } from "../components/dropdowns";
19-
import { formattedTime } from "../utils";
20+
import { buildRequest, formattedTime } from "../utils";
2021

2122
export class MembershipRequestsResultItem extends Component {
2223
constructor(props) {
@@ -25,6 +26,8 @@ export class MembershipRequestsResultItem extends Component {
2526
this.state = { membershipRequest: result };
2627
}
2728

29+
static contextType = MembershipRequestsContext;
30+
2831
update = (data, value) => {
2932
const { membershipRequest } = this.state;
3033
this.setState({ membershipRequest: { ...membershipRequest, ...{ role: value } } });
@@ -39,13 +42,15 @@ export class MembershipRequestsResultItem extends Component {
3942
} = this.props;
4043

4144
const {
42-
membershipRequest: { member, request },
45+
membershipRequest: { member },
4346
membershipRequest,
4447
} = this.state;
45-
// TODO: Decision flow
46-
// const { api: membershipRequestsApi } = this.context;
47-
const rolesCanAssignByType = rolesCanAssign[member.type];
48-
const membershipRequestExpiration = formattedTime(request.expires_at);
48+
49+
const request = buildRequest(membershipRequest, ["accept", "decline"]);
50+
const { api: membershipRequestsApi } = this.context;
51+
const roles = rolesCanAssign[member.type];
52+
const expiration = formattedTime(request.expires_at);
53+
4954
return (
5055
<Table.Row className="community-member-item">
5156
<Table.Cell>
@@ -73,17 +78,16 @@ export class MembershipRequestsResultItem extends Component {
7378
<RequestStatus status={request.status} />
7479
</Table.Cell>
7580
<Table.Cell
76-
aria-label={i18next.t("Expires") + " " + membershipRequestExpiration}
81+
aria-label={i18next.t("Expires") + " " + expiration}
7782
data-label={i18next.t("Expires")}
7883
>
79-
{membershipRequestExpiration}
84+
{expiration}
8085
</Table.Cell>
8186
<Table.Cell data-label={i18next.t("Role")}>
8287
<RoleDropdown
83-
roles={rolesCanAssignByType}
88+
roles={roles}
89+
action={membershipRequestsApi.updateRole}
8490
successCallback={this.update}
85-
// TODO: Decision flow
86-
// action={membershipRequestsApi.updateRole}
8791
disabled={!membershipRequest.permissions.can_update_role}
8892
currentValue={membershipRequest.role}
8993
resource={membershipRequest}
@@ -92,9 +96,10 @@ export class MembershipRequestsResultItem extends Component {
9296
</Table.Cell>
9397
<Table.Cell data-label={i18next.t("Actions")}>
9498
<RequestActionController
95-
request={membershipRequest}
96-
// TODO: Decision flow
97-
actionSuccessCallback={() => console.log("actionSuccessCallback called")}
99+
request={request}
100+
actionSuccessCallback={() => {
101+
window.location.reload();
102+
}}
98103
/>
99104
</Table.Cell>
100105
</Table.Row>

Diff for: invenio_communities/assets/semantic-ui/js/invenio_communities/members/membership_requests/index.js

-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ const dataAttr = document.getElementById(
3939
const community = JSON.parse(dataAttr.community);
4040
const communitiesAllRoles = JSON.parse(dataAttr.communitiesAllRoles);
4141
const communitiesRolesCanAssign = JSON.parse(dataAttr.communitiesRolesCanAssign);
42-
// TODO: Decision flow: do we need?
43-
// const permissions = JSON.parse(dataAttr.permissions);
4442

4543
const appName = "InvenioCommunities.MembershipRequestsSearch";
4644

Diff for: invenio_communities/members/records/api.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ class Member(Record, MemberMixin):
204204

205205

206206
class ArchivedInvitation(Record, MemberMixin):
207-
"""An archived invitation record.
207+
"""An archived invitation or membership request record.
208+
209+
The name is a historical legacy. It could be renamed in the future, but we don't
210+
want to rename the index.
208211
209212
We are using a record without using the actual JSON document and
210213
schema validation normally used in a record. The reason for using a record

Diff for: invenio_communities/members/records/dumpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ def dump(self, record, data):
3030
def load(self, data, record_cls):
3131
"""Load relations.request.type.
3232
33-
TODO: Works without it for now. Potentially revisit?
33+
Works without implementation for now.
3434
"""
3535
pass

Diff for: invenio_communities/members/resources/config.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ class MemberResourceConfig(RecordResourceConfig):
4646
AlreadyMemberError: create_error_handler(
4747
HTTPJSONException(
4848
code=400,
49-
description="A member was already added or invited.",
49+
description=_(
50+
"A membership already exists or is already being assessed."
51+
),
5052
)
5153
),
5254
CommunityDeletedError: create_error_handler(

Diff for: invenio_communities/members/resources/resource.py

+14
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def create_url_rules(self):
3939
route(
4040
"GET", routes["membership_requests"], self.search_membership_requests
4141
),
42+
route(
43+
"PUT", routes["membership_requests"], self.update_membership_requests
44+
),
4245
]
4346

4447
@request_view_args
@@ -160,3 +163,14 @@ def search_membership_requests(self):
160163
search_preference=search_preference(),
161164
)
162165
return hits.to_dict(), 200
166+
167+
@request_view_args
168+
@request_extra_args
169+
@request_data
170+
def update_membership_requests(self):
171+
"""Update membership request.
172+
173+
From the outside, a membership request is its own resource.
174+
From the inside, it's just a member when it comes to update.
175+
"""
176+
return self.update()

Diff for: invenio_communities/members/services/components.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ def _member_changed(self, member, community=None):
5252
for user_id in user_ids:
5353
on_user_membership_change(Identity(user_id))
5454

55-
def accept_invite(self, identity, record=None, data=None, **kwargs):
56-
"""On accept invite."""
55+
def accept_member_request(self, identity, record=None, data=None, **kwargs):
56+
"""Upon acceptance of a member request (invitation or membership request)."""
5757
self._member_changed(record)
5858

5959
def members_add(self, identity, record=None, community=None, data=None, **kwargs):

Diff for: invenio_communities/members/services/config.py

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class MemberServiceConfig(RecordServiceConfig, ConfiguratorMixin):
182182
search = MemberSearchOptions
183183
search_public = PublicSearchOptions
184184
search_invitations = InvitationsSearchOptions
185+
search_membership_requests = InvitationsSearchOptions # Use same as invitations
185186

186187
links_item = {
187188
"actions": LinksForActionsOfMember(

Diff for: invenio_communities/members/services/request.py

+18-13
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class AcceptAction(actions.AcceptAction):
4747

4848
def execute(self, identity, uow):
4949
"""Execute action."""
50-
service().accept_invite(system_identity, self.request.id, uow=uow)
50+
service().accept_member_request(system_identity, self.request.id, uow=uow)
5151
uow.register(
5252
NotificationOp(
5353
CommunityInvitationAcceptNotificationBuilder.build(self.request)
@@ -61,7 +61,7 @@ class DeclineAction(actions.DeclineAction):
6161

6262
def execute(self, identity, uow):
6363
"""Execute action."""
64-
service().decline_invite(system_identity, self.request.id, uow=uow)
64+
service().close_member_request(system_identity, self.request.id, uow=uow)
6565
uow.register(
6666
NotificationOp(
6767
CommunityInvitationDeclineNotificationBuilder.build(self.request)
@@ -75,7 +75,7 @@ class CancelAction(actions.CancelAction):
7575

7676
def execute(self, identity, uow):
7777
"""Execute action."""
78-
service().decline_invite(system_identity, self.request.id, uow=uow)
78+
service().close_member_request(system_identity, self.request.id, uow=uow)
7979
uow.register(
8080
NotificationOp(
8181
CommunityInvitationCancelNotificationBuilder.build(self.request)
@@ -89,7 +89,7 @@ class ExpireAction(actions.ExpireAction):
8989

9090
def execute(self, identity, uow):
9191
"""Execute action."""
92-
service().decline_invite(system_identity, self.request.id, uow=uow)
92+
service().close_member_request(system_identity, self.request.id, uow=uow)
9393
uow.register(
9494
NotificationOp(
9595
CommunityInvitationExpireNotificationBuilder.build(self.request)
@@ -141,27 +141,32 @@ class CancelMembershipRequestAction(actions.CancelAction):
141141

142142
def execute(self, identity, uow):
143143
"""Execute action."""
144-
service().close_membership_request(system_identity, self.request.id, uow=uow)
144+
service().close_member_request(system_identity, self.request.id, uow=uow)
145145
# TODO: Notification flow: Investigate notifications
146146
super().execute(identity, uow)
147147

148148

149-
class AcceptMembershipRequestAction(actions.AcceptAction):
150-
"""Accept membership request action."""
149+
class DeclineMembershipRequestAction(actions.DeclineAction):
150+
"""Decline membership request action."""
151151

152152
def execute(self, identity, uow):
153153
"""Execute action."""
154-
# TODO: Decision flow: Implement me
155-
pass
154+
service().close_member_request(system_identity, self.request.id, uow=uow)
155+
# TODO: Notification flow: Investigate notifications
156+
super().execute(identity, uow)
156157

157158

158-
class DeclineMembershipRequestAction(actions.DeclineAction):
159-
"""Decline membership request action."""
159+
# TODO: Expiration flow: ExpireAction
160+
161+
162+
class AcceptMembershipRequestAction(actions.AcceptAction):
163+
"""Accept membership request action."""
160164

161165
def execute(self, identity, uow):
162166
"""Execute action."""
163-
# TODO: Decision flow: Implement me
164-
pass
167+
service().accept_member_request(system_identity, self.request.id, uow=uow)
168+
# TODO: Notification flow: Investigate notifications
169+
super().execute(identity, uow)
165170

166171

167172
class MembershipRequestRequestType(RequestType):

Diff for: invenio_communities/members/services/schemas.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,24 @@ def get_permissions(self, obj):
229229

230230

231231
class MembershipRequestDumpSchema(MemberDumpSchema):
232-
"""Schema for dumping membership requests.
233-
234-
TODO: Decision flow: Investigate if can be merged with InvitationDumpSchema
235-
"""
232+
"""Schema for dumping membership requests."""
236233

237234
request = fields.Nested(RequestSchema)
235+
236+
def get_permissions(self, obj):
237+
"""Get permission.
238+
239+
Only permission to see if current identity can_update role is needed.
240+
241+
:param obj: api.Member
242+
"""
243+
is_open = obj["request"]["is_open"]
244+
permission_check = self.context["field_permission_check"]
245+
can_update = permission_check(
246+
"members_update",
247+
community_id=obj.community_id,
248+
member=obj,
249+
)
250+
return {
251+
"can_update_role": is_open and can_update
252+
}

0 commit comments

Comments
 (0)