diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/api/CommunityLinksExtractor.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/api/CommunityLinksExtractor.js
index c831f9f77..67320a86f 100644
--- a/invenio_communities/assets/semantic-ui/js/invenio_communities/api/CommunityLinksExtractor.js
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/api/CommunityLinksExtractor.js
@@ -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;
+ }
}
diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/api/RequestLinksExtractor.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/api/RequestLinksExtractor.js
new file mode 100644
index 000000000..21267ad39
--- /dev/null
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/api/RequestLinksExtractor.js
@@ -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/");
+ }
+}
diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/api/membershipRequests/api.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/api/membershipRequests/api.js
new file mode 100644
index 000000000..c9954d29f
--- /dev/null
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/api/membershipRequests/api.js
@@ -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);
+ };
+}
diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/community/header/RequestMembershipButton.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/community/header/RequestMembershipButton.js
new file mode 100644
index 000000000..d09c5c8f2
--- /dev/null
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/community/header/RequestMembershipButton.js
@@ -0,0 +1,148 @@
+/*
+ * This file is part of Invenio.
+ * Copyright (C) 2024 CERN.
+ * Copyright (C) 2024 Northwestern University.
+ *
+ * Invenio 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 { i18next } from "@translations/invenio_communities/i18next";
+import { Formik } from "formik";
+import PropTypes from "prop-types";
+import React, { useState } from "react";
+import { TextAreaField } from "react-invenio-forms";
+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 [errorMsg, setErrorMsg] = useState("");
+
+ const { community, isOpen, onClose } = props;
+
+ const onSubmit = async (values, { setSubmitting, setFieldError }) => {
+ /**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);
+
+ const { errors, message } = communityErrorSerializer(error);
+
+ if (message) {
+ setErrorMsg(message);
+ }
+
+ if (errors) {
+ errors.forEach(({ field, messages }) => setFieldError(field, messages[0]));
+ }
+ }
+ };
+
+ return (
+
+ {({ values, isSubmitting, handleSubmit }) => (
+
+ {i18next.t("Request Membership")}
+
+
+
+
+ {errorMsg}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+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);
+ };
+
+ const handleClose = () => {
+ setModalOpen(false);
+ };
+
+ return (
+ <>
+
+ {isModalOpen && (
+
+ )}
+ >
+ );
+}
+
+RequestMembershipButton.propTypes = {
+ community: PropTypes.object.isRequired,
+};
diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/community/header/index.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/community/header/index.js
new file mode 100644
index 000000000..f16b1c1f0
--- /dev/null
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/community/header/index.js
@@ -0,0 +1,21 @@
+/*
+ * This file is part of Invenio.
+ * Copyright (C) 2024 Northwestern University.
+ *
+ * Invenio 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 ReactDOM from "react-dom";
+
+import React from "react";
+
+import { RequestMembershipButton } from "./RequestMembershipButton";
+
+const domContainer = document.getElementById("request-membership-app");
+
+const community = JSON.parse(domContainer.dataset.community);
+
+if (domContainer) {
+ ReactDOM.render(, domContainer);
+}
diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/CommunityPriviledgesForm.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/CommunityPrivilegesForm.js
similarity index 70%
rename from invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/CommunityPriviledgesForm.js
rename to invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/CommunityPrivilegesForm.js
index ce4d30036..c609ff091 100644
--- a/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/CommunityPriviledgesForm.js
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/CommunityPrivilegesForm.js
@@ -10,6 +10,7 @@
import { i18next } from "@translations/invenio_communities/i18next";
import { CommunitySettingsForm } from "..//components/CommunitySettingsForm";
import _get from "lodash/get";
+import _isEmpty from "lodash/isEmpty";
import { useField } from "formik";
import React, { Component } from "react";
import { RadioField } from "react-invenio-forms";
@@ -18,17 +19,31 @@ import PropTypes from "prop-types";
const VisibilityField = ({ label, formConfig, ...props }) => {
const [field] = useField(props);
+ const fieldPath = "access.visibility";
+
+ function createHandleChange(radioValue) {
+ function handleChange({ event, data, formikProps }) {
+ formikProps.form.setFieldValue(fieldPath, radioValue);
+ // dependent fields
+ if (radioValue === "restricted") {
+ formikProps.form.setFieldValue("access.member_policy", "closed");
+ }
+ }
+ return handleChange;
+ }
+
return (
<>
{formConfig.access.visibility.map((item) => (
@@ -76,14 +91,47 @@ MembersVisibilityField.defaultProps = {
label: "",
};
+const MemberPolicyField = ({ label, formConfig, ...props }) => {
+ const [field] = useField(props);
+ const isDisabled = _get(field.value, "access.visibility") === "restricted";
+
+ return (
+ <>
+ {formConfig.access.member_policy.map((item) => (
+
+
+
+
+ ))}
+ >
+ );
+};
+
+MemberPolicyField.propTypes = {
+ label: PropTypes.string,
+ formConfig: PropTypes.object.isRequired,
+};
+
+MemberPolicyField.defaultProps = {
+ label: "",
+};
+
class CommunityPrivilegesForm extends Component {
getInitialValues = () => {
return {
access: {
visibility: "public",
members_visibility: "public",
+ member_policy: "closed",
// TODO: Re-enable once properly integrated to be displayed
- // member_policy: "open",
// record_policy: "open",
},
};
@@ -105,6 +153,7 @@ class CommunityPrivilegesForm extends Component {
+
{i18next.t("Members visibility")}
@@ -114,6 +163,19 @@ class CommunityPrivilegesForm extends Component {
+
+ {!_isEmpty(formConfig.access.member_policy) && (
+ <>
+
+ {i18next.t("Membership Policy")}
+
+ {i18next.t("Controls if anyone can request to join your community.")}
+
+
+
+ >
+ )}
+
{/* TODO: Re-enable once properly integrated to be displayed */}
{/*
diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/index.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/index.js
index ecbfcbd2d..fff65cd03 100644
--- a/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/index.js
+++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/settings/privileges/index.js
@@ -1,4 +1,4 @@
-import CommunityPrivilegesForm from "./CommunityPriviledgesForm";
+import CommunityPrivilegesForm from "./CommunityPrivilegesForm";
import ReactDOM from "react-dom";
import React from "react";
diff --git a/invenio_communities/communities/services/config.py b/invenio_communities/communities/services/config.py
index 34cd0c807..fdd266a52 100644
--- a/invenio_communities/communities/services/config.py
+++ b/invenio_communities/communities/services/config.py
@@ -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(
diff --git a/invenio_communities/config.py b/invenio_communities/config.py
index 1e9185740..bb325fb82 100644
--- a/invenio_communities/config.py
+++ b/invenio_communities/config.py
@@ -314,3 +314,6 @@
COMMUNITIES_ALWAYS_SHOW_CREATE_LINK = False
"""Controls visibility of 'New Community' btn based on user's permission when set to True."""
+
+COMMUNITIES_ALLOW_MEMBERSHIP_REQUESTS = False
+"""Feature flag for membership request."""
diff --git a/invenio_communities/generators.py b/invenio_communities/generators.py
index e18a6320f..c945d419b 100644
--- a/invenio_communities/generators.py
+++ b/invenio_communities/generators.py
@@ -17,7 +17,7 @@
from itertools import chain
from flask_principal import UserNeed
-from invenio_access.permissions import any_user, system_process
+from invenio_access.permissions import any_user, authenticated_user, system_process
from invenio_records_permissions.generators import Generator
from invenio_search.engine import dsl
@@ -199,6 +199,28 @@ def query_filter(self, **kwargs):
#
# Community membership generators
#
+
+
+class AuthenticatedButNotCommunityMembers(Generator):
+ """Authenticated user not part of community."""
+
+ def needs(self, record=None, **kwargs):
+ """Required needs."""
+ return [authenticated_user]
+
+ def excludes(self, record=None, **kwargs):
+ """Exluding needs.
+
+ Excludes identities with a role in the community. This assumes all roles at
+ this point mean valid memberships. This is the same assumption as
+ `CommunityMembers` below.
+ """
+ if not record:
+ return []
+ community_id = str(record.id)
+ return [CommunityRoleNeed(community_id, r.name) for r in current_roles]
+
+
class CommunityRoles(Generator):
"""Base class for community roles generators."""
diff --git a/invenio_communities/members/resources/config.py b/invenio_communities/members/resources/config.py
index 8e63d54df..29eb164fa 100644
--- a/invenio_communities/members/resources/config.py
+++ b/invenio_communities/members/resources/config.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 KTH Royal Institute of Technology
-# Copyright (C) 2022 Northwestern University.
+# Copyright (C) 2022-2024 Northwestern University.
# Copyright (C) 2022 CERN.
# Copyright (C) 2023 TU Wien.
#
@@ -29,6 +29,7 @@ class MemberResourceConfig(RecordResourceConfig):
"members": "/communities//members",
"publicmembers": "/communities//members/public",
"invitations": "/communities//invitations",
+ "membership_requests": "/communities//membership-requests",
}
request_view_args = {
"pid_value": ma.fields.UUID(),
diff --git a/invenio_communities/members/resources/resource.py b/invenio_communities/members/resources/resource.py
index ee5a2fa55..d65828887 100644
--- a/invenio_communities/members/resources/resource.py
+++ b/invenio_communities/members/resources/resource.py
@@ -31,10 +31,11 @@ def create_url_rules(self):
route("DELETE", routes["members"], self.delete),
route("PUT", routes["members"], self.update),
route("GET", routes["members"], self.search),
- route("POST", routes["invitations"], self.invite),
route("GET", routes["publicmembers"], self.search_public),
+ route("POST", routes["invitations"], self.invite),
route("PUT", routes["invitations"], self.update_invitations),
route("GET", routes["invitations"], self.search_invitations),
+ route("POST", routes["membership_requests"], self.request_membership),
]
@request_view_args
@@ -98,6 +99,17 @@ def invite(self):
)
return "", 204
+ @request_view_args
+ @request_data
+ def request_membership(self):
+ """Request membership."""
+ request = self.service.request_membership(
+ g.identity,
+ resource_requestctx.view_args["pid_value"],
+ resource_requestctx.data,
+ )
+ return request.to_dict(), 201
+
@request_view_args
@request_extra_args
@request_data
diff --git a/invenio_communities/members/services/request.py b/invenio_communities/members/services/request.py
index 04dec909f..7d193c418 100644
--- a/invenio_communities/members/services/request.py
+++ b/invenio_communities/members/services/request.py
@@ -29,6 +29,11 @@ def service():
return current_communities.service.members
+#
+# CommunityInvitation: actions and request type
+#
+
+
#
# Actions
#
@@ -124,3 +129,37 @@ class CommunityInvitation(RequestType):
"manager",
]
}
+
+
+#
+# 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."""
+
+ type_id = "community-membership-request"
+ name = _("Membership request")
+
+ create_action = "create"
+ available_actions = {
+ "create": actions.CreateAndSubmitAction,
+ "cancel": CancelMembershipRequestAction,
+ }
+
+ creator_can_be_none = False
+ topic_can_be_none = False
+ allowed_creator_ref_types = ["user"]
+ allowed_receiver_ref_types = ["community"]
+ allowed_topic_ref_types = ["community"]
diff --git a/invenio_communities/members/services/schemas.py b/invenio_communities/members/services/schemas.py
index bc73c64e1..ad9f9f71f 100644
--- a/invenio_communities/members/services/schemas.py
+++ b/invenio_communities/members/services/schemas.py
@@ -93,6 +93,12 @@ class DeleteBulkSchema(MembersSchema):
"""Delete bulk schema."""
+class RequestMembershipSchema(Schema):
+ """Schema used for requesting membership."""
+
+ message = SanitizedUnicode()
+
+
#
# Schemas used for dumping a single member
#
diff --git a/invenio_communities/members/services/service.py b/invenio_communities/members/services/service.py
index e911d9dfd..24255467b 100644
--- a/invenio_communities/members/services/service.py
+++ b/invenio_communities/members/services/service.py
@@ -40,7 +40,7 @@
from ...proxies import current_roles
from ..errors import AlreadyMemberError, InvalidMemberError
from ..records.api import ArchivedInvitation
-from .request import CommunityInvitation
+from .request import CommunityInvitation, MembershipRequestRequestType
from .schemas import (
AddBulkSchema,
DeleteBulkSchema,
@@ -48,6 +48,7 @@
InviteBulkSchema,
MemberDumpSchema,
PublicDumpSchema,
+ RequestMembershipSchema,
UpdateBulkSchema,
)
@@ -103,6 +104,11 @@ def delete_schema(self):
"""Schema for bulk delete."""
return ServiceSchemaWrapper(self, schema=DeleteBulkSchema)
+ @property
+ def request_membership_schema(self):
+ """Wrapped schema for request membership."""
+ return ServiceSchemaWrapper(self, schema=RequestMembershipSchema)
+
@property
def archive_indexer(self):
"""Factory for creating an indexer instance."""
@@ -734,3 +740,120 @@ def rebuild_index(self, identity, uow=None):
self.archive_indexer.bulk_index([inv.id for inv in archived_invitations])
return True
+
+ # Request membership
+ @unit_of_work()
+ def request_membership(self, identity, community_id, data, uow=None):
+ """Request membership to the community.
+
+ A user can only have one request per community.
+
+ All validations raise, so it's up to parent layer to handle them.
+ """
+ community = self.community_cls.get_record(community_id)
+
+ data, errors = self.request_membership_schema.load(
+ data,
+ context={"identity": identity},
+ )
+ message = data.get("message", "")
+
+ self.require_permission(
+ identity,
+ "request_membership",
+ record=community,
+ )
+
+ # Create request
+ title = _('Request to join "{community}"').format(
+ community=community.metadata["title"],
+ )
+ request_item = current_requests_service.create(
+ identity,
+ data={
+ "title": title,
+ # "description": description,
+ },
+ request_type=MembershipRequestRequestType,
+ receiver=community,
+ creator={"user": str(identity.user.id)},
+ topic=community, # user instead?
+ # TODO: Consider expiration
+ # expires_at=invite_expires_at(),
+ uow=uow,
+ )
+
+ if message:
+ data = {"payload": {"content": message}}
+ current_events_service.create(
+ identity,
+ request_item.id,
+ data,
+ CommentEventType,
+ uow=uow,
+ notify=False,
+ )
+
+ # TODO: Add notification mechanism
+ # uow.register(
+ # NotificationOp(
+ # MembershipRequestSubmittedNotificationBuilder.build(
+ # request=request_item._request,
+ # # explicit string conversion to get the value of LazyText
+ # role=str(role.title),
+ # message=message,
+ # )
+ # )
+ # )
+
+ # Create an inactive member entry linked to the request.
+ self._add_factory(
+ identity,
+ community=community,
+ role=current_roles["reader"],
+ visible=False,
+ member={"type": "user", "id": str(identity.user.id)},
+ message=message,
+ uow=uow,
+ active=False,
+ request_id=request_item.id,
+ )
+
+ # No registered component with a request_membership method for now,
+ # so no run_components for now.
+
+ # Has to return the request so that frontend can redirect to it
+ return request_item
+
+ @unit_of_work()
+ def update_membership_request(self, identity, community_id, data, uow=None):
+ """Update membership request."""
+ # TODO: Implement me
+ pass
+
+ def search_membership_requests(self):
+ """Search membership requests."""
+ # TODO: Implement me
+ pass
+
+ @unit_of_work()
+ def accept_membership_request(self, identity, request_id, uow=None):
+ """Accept membership request."""
+ # TODO: Implement me
+ pass
+
+ @unit_of_work()
+ 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))
diff --git a/invenio_communities/permissions.py b/invenio_communities/permissions.py
index 196912233..b67d7f3b4 100644
--- a/invenio_communities/permissions.py
+++ b/invenio_communities/permissions.py
@@ -25,6 +25,7 @@
from .generators import (
AllowedMemberTypes,
+ AuthenticatedButNotCommunityMembers,
CommunityCurators,
CommunityManagers,
CommunityManagersForRole,
@@ -179,9 +180,26 @@ class CommunityPermissionPolicy(BasePermissionPolicy):
# Permissions to set if communities can have children
can_manage_children = [SystemProcess()]
- # Permission for assinging a parent community
+ # Permission for assigning a parent community
can_manage_parent = [Administration(), SystemProcess()]
+ # request_membership permission is based on configuration, community settings and
+ # identity. Other factors (e.g., previous membership requests) are not under
+ # its purview and are dealt with elsewhere.
+ can_request_membership = [
+ IfConfig(
+ "COMMUNITIES_ALLOW_MEMBERSHIP_REQUESTS",
+ then_=[
+ IfPolicyClosed(
+ "member_policy",
+ then_=[Disable()],
+ else_=[AuthenticatedButNotCommunityMembers()],
+ ),
+ ],
+ else_=[Disable()],
+ ),
+ ]
+
def can_perform_action(community, context):
"""Check if the given action is available on the request."""
diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html b/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html
index 55ac48129..1f4d51eb8 100644
--- a/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html
+++ b/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html
@@ -9,6 +9,11 @@
{% extends "invenio_communities/base.html" %}
+{%- block javascript %}
+{{ super() }}
+{{ webpack['invenio-communities-header.js'] }}
+{%- endblock javascript %}
+
{%- block page_body %}
{% set community_menu_active = True %}
{% include "invenio_communities/details/header.html" %}
diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html b/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html
index fe36a96ae..0f5f9ebbf 100644
--- a/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html
+++ b/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html
@@ -2,6 +2,7 @@
This file is part of Invenio.
Copyright (C) 2016-2020 CERN.
+ Copyright (C) 2024 Northwestern University.
Invenio is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
@@ -10,12 +11,25 @@
{%- from "invenio_theme/macros/truncate.html" import truncate_text %}
{%- from "invenio_communities/details/macros/access-status-label.html" import access_status_label -%}
+{% macro button_to_request_membership(community) %}
+ {% if permissions.can_request_membership %}
+ {# TODO: Add relation_to_community for other flows #}
+
+
+ {% endif %}
+{% endmacro %}
+
+