Skip to content

Commit

Permalink
feat: Group GQL, added membership mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
josebui committed Nov 22, 2023
1 parent fd8d83e commit 827c0db
Show file tree
Hide file tree
Showing 5 changed files with 951 additions and 3 deletions.
31 changes: 29 additions & 2 deletions terraso_backend/apps/core/permission_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,27 @@ def allowed_to_change_landscape_membership(user, obj):


@rules.predicate
def allowed_to_add_membership(user, group):
return group.can_join or group.is_manager(user)
def allowed_to_change_group_membership(user, obj):
from apps.collaboration.models import Membership as CollaborationMembership

group = obj.get("group")
current_membership = obj.get("current_membership")
new_membership_status = obj.get("membership_status")
manager_role = get_manager_role(group)
is_manager = group.membership_list.has_role(user, manager_role)

is_approving = (
current_membership
and current_membership.membership_status == CollaborationMembership.PENDING
and new_membership_status == CollaborationMembership.APPROVED
)

if is_approving and not is_manager:
return False

return validate_change_membership(user, group, obj)


def validate_delete_membership(user, entity, membership):
manager_role = get_manager_role(entity)
is_manager = entity.membership_list.has_role(user, manager_role)
Expand All @@ -160,6 +179,12 @@ def allowed_to_delete_landscape_membership(user, obj):
return validate_delete_membership(user, landscape, membership)


@rules.predicate
def allowed_to_delete_group_membership(user, obj):
group = obj.get("group")
membership = obj.get("membership")

return validate_delete_membership(user, group, membership)


rules.add_rule("allowed_group_managers_count", allowed_group_managers_count)
Expand All @@ -168,3 +193,5 @@ def allowed_to_delete_landscape_membership(user, obj):
rules.add_rule("allowed_to_change_landscape_membership", allowed_to_change_landscape_membership)
rules.add_rule("allowed_to_delete_landscape_membership", allowed_to_delete_landscape_membership)
rules.add_rule("allowed_landscape_managers_count", allowed_landscape_managers_count)
rules.add_rule("allowed_to_change_group_membership", allowed_to_change_group_membership)
rules.add_rule("allowed_to_delete_group_membership", allowed_to_delete_group_membership)
6 changes: 6 additions & 0 deletions terraso_backend/apps/graphql/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
GroupNode,
GroupUpdateMutation,
)
from .groups_memberships import (
GroupMembershipDeleteMutation,
GroupMembershipSaveMutation,
)
from .landscape_groups import (
LandscapeGroupAddMutation,
LandscapeGroupDeleteMutation,
Expand Down Expand Up @@ -196,6 +200,8 @@ class Mutations(graphene.ObjectType):
delete_site_note = SiteNoteDeleteMutation.Field()
save_landscape_membership = LandscapeMembershipSaveMutation.Field()
delete_landscape_membership = LandscapeMembershipDeleteMutation.Field()
save_group_membership = GroupMembershipSaveMutation.Field()
delete_group_membership = GroupMembershipDeleteMutation.Field()


schema = graphene.Schema(query=Query, mutation=Mutations)
183 changes: 183 additions & 0 deletions terraso_backend/apps/graphql/schema/groups_memberships.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Copyright © 2023 Technology Matters
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see https://www.gnu.org/licenses/.

import graphene
import rules
import structlog

from apps.collaboration.graphql import (
BaseMembershipSaveMutation,
CollaborationMembershipNode,
)
from apps.collaboration.models import Membership as CollaborationMembership
from apps.collaboration.models import MembershipList
from apps.core import group_collaboration_roles
from apps.core.models import Group
from apps.graphql.exceptions import GraphQLNotAllowedException, GraphQLNotFoundException
from apps.graphql.schema.groups import GroupNode
from apps.notifications.email import EmailNotification

from .commons import BaseDeleteMutation
from .constants import MutationTypes

logger = structlog.get_logger(__name__)


class GroupMembershipSaveMutation(BaseMembershipSaveMutation):
model_class = CollaborationMembership
memberships = graphene.Field(graphene.List(CollaborationMembershipNode))
group = graphene.Field(GroupNode)

class Input:
user_role = graphene.String()
user_emails = graphene.List(graphene.String, required=True)
group_slug = graphene.String(required=True)
membership_status = graphene.String()

@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user

user_role = (
kwargs["user_role"] if "user_role" in kwargs else group_collaboration_roles.ROLE_MEMBER
)

cls.validate_role(user_role, group_collaboration_roles.ALL_ROLES)

group_slug = kwargs["group_slug"]

try:
group = Group.objects.get(slug=group_slug)
except Exception as error:
logger.error(
"Attempt to save Story Map Memberships, but story map was not found",
extra={
"group_slug": group_slug,
"error": error,
},
)
raise GraphQLNotFoundException(model_name=Group.__name__)

is_closed_group = (
group.membership_list.membership_type == MembershipList.MEMBERSHIP_TYPE_CLOSED
)

membership_status = (
CollaborationMembership.APPROVED
if not is_closed_group
else kwargs["membership_status"]
if "membership_status" in kwargs
else CollaborationMembership.PENDING
)

memberships_save_result = cls.save_memberships(
user=user,
validation_rule="allowed_to_change_group_membership",
validation_context={"group": group},
membership_list=group.membership_list,
kwargs={
**kwargs,
"user_role": user_role,
"membership_status": membership_status,
},
)

if group.membership_list.membership_type == MembershipList.MEMBERSHIP_TYPE_CLOSED:
for membership_result in memberships_save_result:
context = membership_result["context"]
membership = membership_result["membership"]
if context["is_new"]:
EmailNotification.send_membership_request(membership.user, group)
if context["is_membership_approved"]:
EmailNotification.send_membership_approval(membership.user, group)

return cls(
memberships=[
membership_result["membership"] for membership_result in memberships_save_result
],
group=group,
)


class GroupMembershipDeleteMutation(BaseDeleteMutation):
membership = graphene.Field(CollaborationMembershipNode)
group = graphene.Field(GroupNode)

model_class = CollaborationMembership

class Input:
id = graphene.ID(required=True)
group_slug = graphene.String(required=True)

@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user
membership_id = kwargs["id"]
group_slug = kwargs["group_slug"]

try:
group = Group.objects.get(slug=group_slug)
except Group.DoesNotExist:
logger.error(
"Attempt to delete Group Membership, but group was not found",
extra={"group_slug": group_slug},
)
raise GraphQLNotFoundException(model_name=Group.__name__)

try:
membership = group.membership_list.memberships.get(id=membership_id)
except CollaborationMembership.DoesNotExist:
logger.error(
"Attempt to delete Group Membership, but membership was not found",
extra={"membership_id": membership_id},
)
raise GraphQLNotFoundException(model_name=CollaborationMembership.__name__)

if not rules.test_rule(
"allowed_to_delete_group_membership",
user,
{
"group": group,
"membership": membership,
},
):
logger.info(
"Attempt to delete Group Memberships, but user lacks permission",
extra={"user_id": user.pk, "membership_id": membership_id},
)
raise GraphQLNotAllowedException(
model_name=CollaborationMembership.__name__, operation=MutationTypes.DELETE
)

if not rules.test_rule(
"allowed_group_managers_count",
user,
{
"group": group,
"membership": membership,
},
):
logger.info(
"Attempt to update a Membership, but cannot remove last manager",
extra={"user_id": user.pk, "membership_id": membership_id},
)
raise GraphQLNotAllowedException(
model_name=CollaborationMembership.__name__,
operation=MutationTypes.DELETE,
message="manager_count",
)

result = super().mutate_and_get_payload(root, info, **kwargs)
return cls(membership=result.membership, group=group)
5 changes: 4 additions & 1 deletion terraso_backend/apps/notifications/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from django.utils.translation import gettext_lazy as _

from apps.auth.services import JWTService
from apps.core import group_collaboration_roles

TRACKING_PARAMETERS = {"utm_source": "notification", "utm_medium": "email"}

Expand Down Expand Up @@ -64,7 +65,9 @@ def send_membership_request(cls, user, group):

managerList = [
membership.user
for membership in group.memberships.managers_only()
for membership in group.membership_list.memberships.by_role(
group_collaboration_roles.ROLE_MANAGER
)
if membership.user.group_notifications_enabled()
]

Expand Down
Loading

0 comments on commit 827c0db

Please sign in to comment.