Skip to content

Commit

Permalink
js+service: [#855] 4) integrate request flow with frontend [+]
Browse files Browse the repository at this point in the history
This concludes the 2nd flow of the membership request feature.
Remaining flows are
- 'waiting for decision' flow
- 'making a decision' flow

This PR needs to be complemented by:
- one in invenio-requests (done)
- one in invenio-rdm-records (to do)
  • Loading branch information
fenekku committed Jul 30, 2024
1 parent f8727c5 commit 6c1bb8a
Show file tree
Hide file tree
Showing 13 changed files with 559 additions and 335 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ export class CommunityLinksExtractor {
}
return this.#urls.invitations;
}

url(key) {
const urlOfKey = this.#urls[key];
if (!urlOfKey) {
throw TypeError(`"${key}" link missing from resource.`);
}
return urlOfKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// This file is part of Invenio-communities
// Copyright (C) 2024 Northwestern University.
//
// Invenio-communities is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

export class RequestLinksExtractor {
#urls;

constructor(request) {
if (!request?.links) {
throw TypeError("Request resource links are undefined");
}
this.#urls = request.links;
}

url(key) {
const urlOfKey = this.#urls[key];
if (!urlOfKey) {
throw TypeError(`"${key}" link missing from resource.`);
}
return urlOfKey;
}

get userDiscussionUrl() {
const result = this.url("self_html");
return result.replace("/requests/", "/me/requests/");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// This file is part of Invenio-communities
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 Northwestern University.
//
// Invenio-communities is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

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

/**
* API Client for community membership requests.
*
* It mostly uses the API links passed to it from initial community.
*
*/
export class CommunityMembershipRequestsApi {
constructor(community) {
this.community = community;
this.linksExtractor = new CommunityLinksExtractor(community);
}

requestMembership = async (payload) => {
return await http.post(this.linksExtractor.url("membership_requests"), payload);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,49 @@ import { Formik } from "formik";
import PropTypes from "prop-types";
import React, { useState } from "react";
import { TextAreaField } from "react-invenio-forms";
import { Button, Form, Modal } from "semantic-ui-react";
import { Button, Form, Grid, Message, Modal } from "semantic-ui-react";

import { CommunityMembershipRequestsApi } from "../../api/membershipRequests/api";
import { communityErrorSerializer } from "../../api/serializers";
import { RequestLinksExtractor } from "../../api/RequestLinksExtractor";

export function RequestMembershipModal(props) {
const { isOpen, onClose } = props;
const [errorMsg, setErrorMsg] = useState("");

const { community, isOpen, onClose } = props;

const onSubmit = async (values, { setSubmitting, setFieldError }) => {
// TODO: implement me
console.log("RequestMembershipModal.onSubmit(args) called");
console.log("TODO: implement me", arguments);
};
/**Submit callback called from Formik. */
setSubmitting(true);

const client = new CommunityMembershipRequestsApi(community);

try {
const response = await client.requestMembership(values);
const linksExtractor = new RequestLinksExtractor(response.data);
window.location.href = linksExtractor.userDiscussionUrl;
} catch (error) {
setSubmitting(false);

console.log("Error");
console.dir(error);

let confirmed = true;
const { errors, message } = communityErrorSerializer(error);

if (message) {
setErrorMsg(message);
}

if (errors) {
errors.forEach(({ field, messages }) => setFieldError(field, messages[0]));
}
}
};

return (
<Formik
initialValues={{
requestMembershipComment: "",
message: "",
}}
onSubmit={onSubmit}
>
Expand All @@ -42,9 +68,17 @@ export function RequestMembershipModal(props) {
>
<Modal.Header>{i18next.t("Request Membership")}</Modal.Header>
<Modal.Content>
<Message hidden={errorMsg === ""} negative className="flashed">
<Grid container>
<Grid.Column mobile={16} tablet={12} computer={8} textAlign="left">
<strong>{errorMsg}</strong>
</Grid.Column>
</Grid>
</Message>

<Form>
<TextAreaField
fieldPath="requestMembershipComment"
fieldPath="message"
label={i18next.t("Message to managers (optional)")}
/>
</Form>
Expand All @@ -54,12 +88,12 @@ export function RequestMembershipModal(props) {
{i18next.t("Cancel")}
</Button>
<Button
onClick={(event) => {
// TODO: Implement me
console.log("RequestMembershipModal button clicked.");
}}
positive={confirmed}
disabled={isSubmitting}
loading={isSubmitting}
onClick={handleSubmit}
positive
primary
type="button"
>
{i18next.t("Request Membership")}
</Button>
Expand All @@ -73,10 +107,12 @@ export function RequestMembershipModal(props) {
RequestMembershipModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
community: PropTypes.object.isRequired,
};

export function RequestMembershipButton(props) {
const [isModalOpen, setModalOpen] = useState(false);
const { community } = props;

const handleClick = () => {
setModalOpen(true);
Expand All @@ -97,8 +133,16 @@ export function RequestMembershipButton(props) {
content={i18next.t("Request Membership")}
/>
{isModalOpen && (
<RequestMembershipModal isOpen={isModalOpen} onClose={handleClose} />
<RequestMembershipModal
isOpen={isModalOpen}
onClose={handleClose}
community={community}
/>
)}
</>
);
}

RequestMembershipButton.propTypes = {
community: PropTypes.object.isRequired,
};
3 changes: 3 additions & 0 deletions invenio_communities/communities/services/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ class CommunityServiceConfig(RecordServiceConfig, ConfiguratorMixin):
"invitations": CommunityLink("{+api}/communities/{id}/invitations"),
"requests": CommunityLink("{+api}/communities/{id}/requests"),
"records": CommunityLink("{+api}/communities/{id}/records"),
"membership_requests": CommunityLink(
"{+api}/communities/{id}/membership-requests"
),
}

action_link = CommunityLink(
Expand Down
21 changes: 21 additions & 0 deletions invenio_communities/members/services/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def service():
return current_communities.service.members


#
# CommunityInvitation: actions and request type
#


#
# Actions
#
Expand Down Expand Up @@ -126,6 +131,21 @@ class CommunityInvitation(RequestType):
}


#
# MembershipRequestRequestType: actions and request type
#


class CancelMembershipRequestAction(actions.CancelAction):
"""Cancel membership request action."""

def execute(self, identity, uow):
"""Execute action."""
service().close_membership_request(system_identity, self.request.id, uow=uow)
# TODO: Investigate notifications
super().execute(identity, uow)


class MembershipRequestRequestType(RequestType):
"""Request type for membership requests."""

Expand All @@ -135,6 +155,7 @@ class MembershipRequestRequestType(RequestType):
create_action = "create"
available_actions = {
"create": actions.CreateAndSubmitAction,
"cancel": CancelMembershipRequestAction,
}

creator_can_be_none = False
Expand Down
18 changes: 14 additions & 4 deletions invenio_communities/members/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,17 @@ def accept_membership_request(self, identity, request_id, uow=None):
pass

@unit_of_work()
def decline_membership_request(self, identity, request_id, uow=None):
"""Decline membership request."""
# TODO: Implement me
pass
def close_membership_request(self, identity, request_id, uow=None):
"""Close membership request.
Used for cancelling, declining, or expiring a membership request.
For now we just delete the "fake" member that was created in
request_membership. TODO: explore alternatives/ramifications at a
later point.
"""
# Permissions are checked on the request action
assert identity == system_identity
member = self.record_cls.get_member_by_request(request_id)
assert member.active is False
uow.register(RecordDeleteOp(member, indexer=self.indexer, force=True))
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ python_requires = >=3.8
zip_safe = False
install_requires =
invenio-oaiserver>=2.2.0,<3.0.0
invenio-requests>=4.0.0,<5.0.0
invenio-requests>=4.2.0,<5.0.0
invenio-search-ui>=2.4.0,<3.0.0
invenio-vocabularies>=4.0.0,<5.0.0
invenio-administration>=2.0.0,<3.0.0
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def create_user(UserFixture, app, db):
is essential for many tests.
"""

def _create_user(data):
def _create_user(data=None):
"""Create user."""
default_data = dict(
email="[email protected]",
Expand All @@ -391,6 +391,7 @@ def _create_user(data):
active=True,
confirmed=True,
)
data = data or {}
actual_data = dict(default_data, **data)
u = UserFixture(**actual_data)
u.create(app, db)
Expand Down
15 changes: 15 additions & 0 deletions tests/members/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from invenio_access.permissions import system_identity
from invenio_requests.records.api import Request
from invenio_search import current_search
from invenio_users_resources.proxies import current_users_service

from invenio_communities.members.records.api import ArchivedInvitation, Member

Expand Down Expand Up @@ -93,3 +94,17 @@ def invite_request_id(requests_service, invite_user):
type="community-invitation",
).to_dict()
return res["hits"]["hits"][0]["id"]


@pytest.fixture(scope="function")
def membership_request(member_service, community, create_user, db, search_clear):
"""A membership request."""
user = create_user()
data = {
"message": "Can I join the club?",
}
return member_service.request_membership(
user.identity,
community._record.id,
data,
)
Loading

0 comments on commit 6c1bb8a

Please sign in to comment.