🛂(backend) can update role invitation
Allow to update role invitation if owner or admin.
This commit is contained in:
@@ -538,6 +538,7 @@ class InvitationViewset(
|
|||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""API ViewSet for user invitations to document.
|
"""API ViewSet for user invitations to document.
|
||||||
@@ -551,8 +552,9 @@ class InvitationViewset(
|
|||||||
- role: str [administrator|editor|reader]
|
- role: str [administrator|editor|reader]
|
||||||
Return newly created invitation (issuer and document are automatically set)
|
Return newly created invitation (issuer and document are automatically set)
|
||||||
|
|
||||||
PUT / PATCH : Not permitted. Instead of updating your invitation,
|
PATCH /api/v1.0/documents/<document_id>/invitations/:<invitation_id>/ with expected data:
|
||||||
delete and create a new one.
|
- role: str [owner|admin|editor|reader]
|
||||||
|
Return partially updated document invitation
|
||||||
|
|
||||||
DELETE /api/v1.0/documents/<document_id>/invitations/<invitation_id>/
|
DELETE /api/v1.0/documents/<document_id>/invitations/<invitation_id>/
|
||||||
Delete targeted invitation
|
Delete targeted invitation
|
||||||
|
|||||||
@@ -742,13 +742,6 @@ class Invitation(BaseModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.email} invited to {self.document}"
|
return f"{self.email} invited to {self.document}"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""Make invitations read-only."""
|
|
||||||
if self.created_at:
|
|
||||||
raise exceptions.PermissionDenied()
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Validate fields."""
|
"""Validate fields."""
|
||||||
super().clean()
|
super().clean()
|
||||||
@@ -771,6 +764,7 @@ class Invitation(BaseModel):
|
|||||||
def get_abilities(self, user):
|
def get_abilities(self, user):
|
||||||
"""Compute and return abilities for a given user."""
|
"""Compute and return abilities for a given user."""
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
can_update = False
|
||||||
roles = []
|
roles = []
|
||||||
|
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
@@ -789,9 +783,13 @@ class Invitation(BaseModel):
|
|||||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
can_update = bool(
|
||||||
|
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"destroy": can_delete,
|
"destroy": can_delete,
|
||||||
"update": False,
|
"update": can_update,
|
||||||
"partial_update": False,
|
"partial_update": can_update,
|
||||||
"retrieve": bool(roles),
|
"retrieve": bool(roles),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,8 +341,8 @@ def test_api_document_invitations__list__authenticated(
|
|||||||
"is_expired": False,
|
"is_expired": False,
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"destroy": role in ["administrator", "owner"],
|
"destroy": role in ["administrator", "owner"],
|
||||||
"update": False,
|
"update": role in ["administrator", "owner"],
|
||||||
"partial_update": False,
|
"partial_update": role in ["administrator", "owner"],
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -393,8 +393,8 @@ def test_api_document_invitations__list__expired_invitations_still_listed(settin
|
|||||||
"is_expired": True,
|
"is_expired": True,
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"destroy": True,
|
"destroy": True,
|
||||||
"update": False,
|
"update": True,
|
||||||
"partial_update": False,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -468,21 +468,17 @@ def test_api_document_invitations__retrieve__document_member(via, mock_user_get_
|
|||||||
"is_expired": False,
|
"is_expired": False,
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"destroy": role in ["administrator", "owner"],
|
"destroy": role in ["administrator", "owner"],
|
||||||
"update": False,
|
"update": role in ["administrator", "owner"],
|
||||||
"partial_update": False,
|
"partial_update": role in ["administrator", "owner"],
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
@pytest.mark.parametrize(
|
def test_api_document_invitations__put_authenticated(via, mock_user_get_teams):
|
||||||
"method",
|
|
||||||
["put", "patch"],
|
|
||||||
)
|
|
||||||
def test_api_document_invitations__update__forbidden(method, via, mock_user_get_teams):
|
|
||||||
"""
|
"""
|
||||||
Update of invitations is currently forbidden.
|
Authenticated user can put invitations.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
invitation = factories.InvitationFactory()
|
invitation = factories.InvitationFactory()
|
||||||
@@ -496,6 +492,78 @@ def test_api_document_invitations__update__forbidden(method, via, mock_user_get_
|
|||||||
document=invitation.document, team="lasuite", role="owner"
|
document=invitation.document, team="lasuite", role="owner"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||||
|
response = client.patch(url, {"email": "test@test.test"}, format="json")
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
invitation.refresh_from_db()
|
||||||
|
assert invitation.email == "test@test.test"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_invitations__patch_authenticated(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Authenticated user can patch invitations.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
invitation = factories.InvitationFactory(role="owner")
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=invitation.document, user=user, role="owner"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=invitation.document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invitation.role == "owner"
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||||
|
response = client.patch(
|
||||||
|
url,
|
||||||
|
{"role": "reader"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
invitation.refresh_from_db()
|
||||||
|
assert invitation.role == "reader"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"method",
|
||||||
|
["put", "patch"],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"role",
|
||||||
|
["editor", "reader"],
|
||||||
|
)
|
||||||
|
def test_api_document_invitations__update__forbidden__not_authenticated(
|
||||||
|
method, via, role, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update of invitations is currently forbidden.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
invitation = factories.InvitationFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=invitation.document, user=user, role=role
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=invitation.document, team="lasuite", role=role
|
||||||
|
)
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||||
@@ -504,8 +572,11 @@ def test_api_document_invitations__update__forbidden(method, via, mock_user_get_
|
|||||||
if method == "patch":
|
if method == "patch":
|
||||||
response = client.patch(url)
|
response = client.patch(url)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
assert response.json()["detail"] == f'Method "{method.upper()}" not allowed.'
|
assert (
|
||||||
|
response.json()["detail"]
|
||||||
|
== "You do not have permission to perform this action."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_invitations__delete__anonymous():
|
def test_api_document_invitations__delete__anonymous():
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ pytestmark = pytest.mark.django_db
|
|||||||
fake = Faker()
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
def test_models_invitations_readonly_after_create():
|
|
||||||
"""Existing invitations should be readonly."""
|
|
||||||
invitation = factories.InvitationFactory()
|
|
||||||
with pytest.raises(exceptions.PermissionDenied):
|
|
||||||
invitation.save()
|
|
||||||
|
|
||||||
|
|
||||||
def test_models_invitations_email_no_empty_mail():
|
def test_models_invitations_email_no_empty_mail():
|
||||||
"""The "email" field should not be empty."""
|
"""The "email" field should not be empty."""
|
||||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
||||||
@@ -217,8 +210,8 @@ def test_models_document_invitations_get_abilities_privileged_member(
|
|||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": True,
|
"destroy": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"partial_update": False,
|
"partial_update": True,
|
||||||
"update": False,
|
"update": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user