Skip to content

Commit

Permalink
chore: Merge remote-tracking branch 'origin/main' into feat/split-pro…
Browse files Browse the repository at this point in the history
…ject-mlist-790
  • Loading branch information
David Code Howard committed Sep 19, 2023
2 parents 2a0f7ca + 60e0f2a commit e0623b1
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 14 deletions.
6 changes: 3 additions & 3 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,9 @@ mixer==7.2.2 \
--hash=sha256:8089b8e2d00288c77e622936198f5dd03c8ac1603a1530a4f870dc213363b2ae \
--hash=sha256:9b3f1a261b56d8f2394f39955f83adbc7ff3ab4bb1065ebfec19a10d3e8501e0
# via -r requirements/dev.in
moto==4.2.2 \
--hash=sha256:2a9cbcd9da1a66b23f95d62ef91968284445233a606b4de949379395056276fb \
--hash=sha256:ee34c4c3f53900d953180946920c9dba127a483e2ed40e6dbf93d4ae2e760e7c
moto==4.2.3 \
--hash=sha256:2e934d834729b274382055e097b166127db829ab4fae00bb08c031c108391a2c \
--hash=sha256:4caab0145d557d102fe79d0ce3b73d6bf1d916d29ad03c14da15f7da66429cdb
# via -r requirements/dev.in
mypy-extensions==1.0.0 \
--hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
Expand Down
6 changes: 6 additions & 0 deletions terraso_backend/apps/graphql/schema/memberships.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from apps.graphql.exceptions import GraphQLNotAllowedException, GraphQLNotFoundException
from apps.notifications.email import EmailNotification

from ..signals import membership_added_signal, membership_updated_signal
from .commons import BaseAuthenticatedMutation, BaseDeleteMutation, TerrasoConnection
from .constants import MutationTypes

Expand Down Expand Up @@ -75,6 +76,7 @@ class Input:

@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
request_user = info.context.user
user_email = kwargs.pop("user_email")
group_slug = kwargs.pop("group_slug")

Expand Down Expand Up @@ -118,6 +120,8 @@ def mutate_and_get_payload(cls, root, info, **kwargs):
membership.user_role = user_role
membership.save()

membership_added_signal.send(sender=cls, membership=membership, user=request_user)

return cls(membership=membership)


Expand Down Expand Up @@ -181,6 +185,8 @@ def mutate_and_get_payload(cls, root, info, **kwargs):

membership.save()

membership_updated_signal.send(sender=cls, membership=membership, user=user)

return cls(membership=membership)


Expand Down
2 changes: 1 addition & 1 deletion terraso_backend/apps/graphql/schema/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def mutate_and_get_payload(cls, root, info, **kwargs):
continue
metadata[key] = value
if project_id:
metadata["project_name"] = project.name
metadata["project_id"] = str(project.id)

log.log(
user=user,
Expand Down
20 changes: 20 additions & 0 deletions terraso_backend/apps/graphql/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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/.

from django.dispatch import Signal

membership_added_signal = Signal()
membership_updated_signal = Signal()
membership_deleted_signal = Signal()
3 changes: 3 additions & 0 deletions terraso_backend/apps/project_management/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@
class ProjectManagementConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.project_management"

def ready(self):
from .signals import handle_membership_added, handle_membership_updated # noqa
44 changes: 37 additions & 7 deletions terraso_backend/apps/project_management/graphql/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
TerrasoConnection,
)
from apps.graphql.schema.constants import MutationTypes
from apps.graphql.signals import (
membership_added_signal,
membership_deleted_signal,
membership_updated_signal,
)
from apps.project_management.models import Project
from apps.project_management.models.sites import Site

Expand Down Expand Up @@ -99,7 +104,11 @@ def mutate_and_get_payload(cls, root, info, **kwargs):
if not client_time:
client_time = datetime.now()
action = log_api.CREATE
metadata = {"name": kwargs["name"], "privacy": kwargs["privacy"]}
metadata = {
"name": kwargs["name"],
"privacy": kwargs["privacy"],
"description": kwargs.get("description"),
}
logger.log(
user=user,
action=action,
Expand Down Expand Up @@ -179,12 +188,27 @@ class Input:
@classmethod
@transaction.atomic
def mutate_and_get_payload(cls, root, info, **kwargs):
logger = cls.get_logger()
user = info.context.user
project_id = kwargs["id"]
project = cls.get_or_throw(Project, "id", project_id)
if not user.has_perm(Project.get_perm("change"), project):
cls.not_allowed()
kwargs["privacy"] = kwargs["privacy"].value

metadata = {
"name": kwargs["name"],
"privacy": kwargs["privacy"],
"description": kwargs["description"] if "description" in kwargs else None,
}
logger.log(
user=user,
action=log_api.CHANGE,
resource=project,
client_time=datetime.now(),
metadata=metadata,
)

return super().mutate_and_get_payload(root, info, **kwargs)


Expand All @@ -205,15 +229,15 @@ def mutate_and_get_payload(cls, root, info, project_id, user_id, role):
raise GraphQLValidationException(message=f"Invalid role: {role}")
project = cls.get_or_throw(Project, "project_id", project_id)
user = cls.get_or_throw(User, "user_id", user_id)
current_user = info.context.user
requester_membership = project.get_membership(current_user)
request_user = info.context.user
requester_membership = project.get_membership(request_user)
if not requester_membership:
cls.not_allowed_create(model=Membership, msg="User does not belong to project")

def validate(context):
if not rules.test_rule(
"allowed_to_add_member_to_project",
current_user,
request_user,
{"project": project, "requester_membership": requester_membership},
):
raise ValidationError("User cannot add membership to this project")
Expand All @@ -229,6 +253,8 @@ def validate(context):
except ValidationError as e:
cls.not_allowed_create(model=Membership, msg=e.message)

membership_added_signal.send(sender=cls, membership=membership, user=request_user)

return ProjectAddUserMutation(project=project, membership=membership)


Expand All @@ -247,8 +273,8 @@ def mutate_and_get_payload(cls, root, info, project_id, user_id):
# check if user has proper permissions
project = cls.get_or_throw(Project, "project_id", project_id)
user = cls.get_or_throw(User, "user_id", user_id)
current_user = info.context.user
requester_membership = project.get_membership(current_user)
requester = info.context.user
requester_membership = project.get_membership(requester)
if not requester_membership:
cls.not_allowed(
MutationTypes.DELETE,
Expand All @@ -261,7 +287,7 @@ def mutate_and_get_payload(cls, root, info, project_id, user_id):
)
if not rules.test_rule(
"allowed_to_delete_user_from_project",
current_user,
requester,
{
"project": project,
"requester_membership": requester_membership,
Expand All @@ -275,6 +301,8 @@ def mutate_and_get_payload(cls, root, info, project_id, user_id):
membership = project.get_membership(user)
membership.delete()

membership_deleted_signal.send(sender=cls, membership=target_membership, user=requester)

return ProjectDeleteUserMutation(project=project, membership=membership)


Expand Down Expand Up @@ -318,4 +346,6 @@ def mutate_and_get_payload(cls, root, info, project_id, user_id, new_role):
target_membership.user_role = new_role
target_membership.save()

membership_updated_signal.send(sender=cls, membership=target_membership, user=requester)

return ProjectUpdateUserRoleMutation(project=project, membership=target_membership)
75 changes: 75 additions & 0 deletions terraso_backend/apps/project_management/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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/.

from datetime import datetime

from django.dispatch import receiver

from apps.audit_logs import api as audit_log_api
from apps.audit_logs import services
from apps.graphql.signals import (
membership_added_signal,
membership_deleted_signal,
membership_updated_signal,
)

audit_logger = services.new_audit_logger()


def _handle_membership_log(user, action, membership, client_time):
try:
project = membership.membership_list.project
except Exception:
# No project for membership, do nothing
return

audit_logger.log(
user=user,
action=action,
resource=membership,
metadata={
"user_email": membership.user.email,
"user_role": membership.user_role,
"project_id": str(project.id),
},
client_time=client_time,
)


@receiver(membership_added_signal)
def handle_membership_added(sender, **kwargs):
membership = kwargs["membership"]
user = kwargs["user"]
client_time = datetime.now()

_handle_membership_log(user, audit_log_api.CREATE, membership, client_time)


@receiver(membership_updated_signal)
def handle_membership_updated(sender, **kwargs):
membership = kwargs["membership"]
user = kwargs["user"]
client_time = datetime.now()

_handle_membership_log(user, audit_log_api.CHANGE, membership, client_time)


@receiver(membership_deleted_signal)
def handle_membership_deleted(sender, **kwargs):
membership = kwargs["membership"]
user = kwargs["user"]
client_time = datetime.now()

_handle_membership_log(user, audit_log_api.DELETE, membership, client_time)
82 changes: 80 additions & 2 deletions terraso_backend/tests/graphql/mutations/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from graphene_django.utils.testing import graphql_query
from mixer.backend.django import mixer

from apps.audit_logs.api import CREATE
from apps.audit_logs.api import CHANGE, CREATE
from apps.audit_logs.models import Log
from apps.core.models.users import User
from apps.project_management.models import Project
Expand Down Expand Up @@ -42,7 +42,11 @@ def test_create_project(client, user):
log_result = logs[0]
assert log_result.event == CREATE.value
assert log_result.resource_object == project
expected_metadata = {"name": "testProject", "privacy": "private"}
expected_metadata = {
"name": "testProject",
"privacy": "private",
"description": "A test project",
}
assert log_result.metadata == expected_metadata


Expand Down Expand Up @@ -155,6 +159,29 @@ def test_update_project_user_is_manager(project, client, project_manager):
assert content["data"]["updateProject"]["project"]["privacy"] == "PRIVATE"


def test_update_project_audit_log(project, client, project_manager):
input = {
"id": str(project.id),
"name": "test_name",
"privacy": "PRIVATE",
"description": "A test project",
}
client.force_login(project_manager)

response = graphql_query(UPDATE_PROJECT_GRAPHQL, input_data=input, client=client)

assert response.status_code == 200

logs = Log.objects.all()
assert len(logs) == 1
log_result = logs[0]
assert log_result.event == CHANGE.value
assert log_result.user_human_readable == project_manager.full_name()
assert log_result.resource_object == project
expected_metadata = {"name": "test_name", "privacy": "private", "description": "A test project"}
assert log_result.metadata == expected_metadata


def test_update_project_user_not_manager(project, client, project_user):
input = {"id": str(project.id), "name": "test_name", "privacy": "PRIVATE"}
client.force_login(project_user)
Expand Down Expand Up @@ -193,6 +220,41 @@ def test_add_user_to_project(project, project_manager, client):
assert project.viewer_memberships.filter(user=user).exists()


def test_add_user_to_project_audit_log(client, project, project_manager, user):
client.force_login(project_manager)

assert project_manager.id != user.id

response = graphql_query(
ADD_USER_GRAPHQL,
variables={
"input": {
"projectId": str(project.id),
"userId": str(user.id),
"role": "viewer",
}
},
client=client,
)

assert response.status_code == 200

membership = project.viewer_memberships.first()

logs = Log.objects.all()
assert len(logs) == 1
log_result = logs[0]
assert log_result.event == CREATE.value
assert log_result.user_human_readable == project_manager.full_name()
assert log_result.resource_object == membership
expected_metadata = {
"user_email": user.email,
"user_role": "viewer",
"project_id": str(project.id),
}
assert log_result.metadata == expected_metadata


def test_add_user_to_project_bad_roles(project, project_manager, client):
user = mixer.blend(User)
client.force_login(project_manager)
Expand Down Expand Up @@ -294,6 +356,22 @@ def test_update_project_role_manager(project, project_manager, project_user, cli
assert payload["data"]["updateUserRoleInProject"]["membership"]["userRole"] == "contributor"
assert project.is_contributor(project_user)
assert not project.is_viewer(project_user)
assert response.status_code == 200

membership = project.get_membership(user=project_user)

logs = Log.objects.all()
assert len(logs) == 1
log_result = logs[0]
assert log_result.event == CHANGE.value
assert log_result.user_human_readable == project_manager.full_name()
assert log_result.resource_object == membership
expected_metadata = {
"user_email": project_user.email,
"user_role": "contributor",
"project_id": str(project.id),
}
assert log_result.metadata == expected_metadata


def test_update_project_role_not_manager(project, project_user, client):
Expand Down
Loading

0 comments on commit e0623b1

Please sign in to comment.