✨(organization) add API endpoints
This provides a way to get information about the organization and update their name for administrators.
This commit is contained in:
@@ -51,6 +51,24 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer):
|
||||
self.fields.pop(field_name)
|
||||
|
||||
|
||||
class OrganizationSerializer(serializers.ModelSerializer):
|
||||
"""Serialize organizations."""
|
||||
|
||||
abilities = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.Organization
|
||||
fields = ["id", "name", "registration_id_list", "domain_list", "abilities"]
|
||||
read_only_fields = ["id", "registration_id_list", "domain_list"]
|
||||
|
||||
def get_abilities(self, organization) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return organization.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
|
||||
class UserSerializer(DynamicFieldsModelSerializer):
|
||||
"""Serialize users."""
|
||||
|
||||
|
||||
@@ -172,6 +172,45 @@ class ContactViewSet(
|
||||
return super().perform_create(serializer)
|
||||
|
||||
|
||||
class OrganizationViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""
|
||||
Organization ViewSet
|
||||
|
||||
GET /api/organizations/<organization_id>/
|
||||
Return the organization details for the given id.
|
||||
|
||||
PUT /api/organizations/<organization_id>/
|
||||
Update the organization details for the given id.
|
||||
|
||||
PATCH /api/organizations/<organization_id>/
|
||||
Partially update the organization details for the given id.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AccessPermission]
|
||||
queryset = models.Organization.objects.all()
|
||||
serializer_class = serializers.OrganizationSerializer
|
||||
throttle_classes = [BurstRateThrottle, SustainedRateThrottle]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Limit listed organizations to the one the user belongs to."""
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(pk=self.request.user.organization_id)
|
||||
.annotate(
|
||||
user_role=Subquery(
|
||||
models.OrganizationAccess.objects.filter(
|
||||
user=self.request.user, organization=OuterRef("pk")
|
||||
).values("role")[:1]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class UserViewSet(
|
||||
SerializerPerActionMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
|
||||
@@ -138,6 +138,22 @@ class OrganizationFactory(factory.django.DjangoModelFactory):
|
||||
)
|
||||
|
||||
|
||||
class OrganizationAccessFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory to create organization accesses for testing purposes."""
|
||||
|
||||
class Meta:
|
||||
model = models.OrganizationAccess
|
||||
|
||||
user = factory.SubFactory(
|
||||
"core.factories.UserFactory",
|
||||
organization=factory.SelfAttribute("..organization"),
|
||||
)
|
||||
organization = factory.SubFactory(
|
||||
"core.factories.OrganizationFactory", with_registration_id=True
|
||||
)
|
||||
role = factory.fuzzy.FuzzyChoice(models.OrganizationRoleChoices.values)
|
||||
|
||||
|
||||
class UserFactory(factory.django.DjangoModelFactory):
|
||||
"""A factory to create random users for testing purposes."""
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ with open(contact_schema_path, "r", encoding="utf-8") as contact_schema_file:
|
||||
contact_schema = json.load(contact_schema_file)
|
||||
|
||||
|
||||
class RoleChoices(models.TextChoices):
|
||||
class RoleChoices(models.TextChoices): # pylint: disable=too-many-ancestors
|
||||
"""Defines the possible roles a user can have in a team."""
|
||||
|
||||
MEMBER = "member", _("Member")
|
||||
@@ -48,7 +48,7 @@ class RoleChoices(models.TextChoices):
|
||||
OWNER = "owner", _("Owner")
|
||||
|
||||
|
||||
class OrganizationRoleChoices(models.TextChoices):
|
||||
class OrganizationRoleChoices(models.TextChoices): # pylint: disable=too-many-ancestors
|
||||
"""
|
||||
Defines the possible roles a user can have in an organization.
|
||||
For now, we only have one role, but we might add more in the future.
|
||||
@@ -355,6 +355,26 @@ class Organization(BaseModel):
|
||||
"domain_list value must be unique across all instances."
|
||||
)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the organization.
|
||||
"""
|
||||
try:
|
||||
# Use the role from queryset annotation if available
|
||||
is_admin = self.user_role == OrganizationRoleChoices.ADMIN
|
||||
except AttributeError:
|
||||
is_admin = self.organization_accesses.filter(
|
||||
user=user,
|
||||
role=OrganizationRoleChoices.ADMIN,
|
||||
).exists()
|
||||
|
||||
return {
|
||||
"get": user.organization_id == self.pk,
|
||||
"patch": is_admin,
|
||||
"put": is_admin,
|
||||
"delete": False,
|
||||
}
|
||||
|
||||
|
||||
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
"""User model to work with OIDC only authentication."""
|
||||
|
||||
1
src/backend/core/tests/organizations/__init__.py
Normal file
1
src/backend/core/tests/organizations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Test organization API endpoints."""
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Tests for Organizations API endpoint in People's core app: retrieve
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_organizations_retrieve_anonymous(client):
|
||||
"""Anonymous users should not be allowed to retrieve an organization."""
|
||||
organization = factories.OrganizationFactory(with_registration_id=True)
|
||||
response = client.get(f"/api/v1.0/organizations/{organization.pk}/")
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_organizations_retrieve_authenticated_unrelated(client):
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve an organization to which they are
|
||||
not related.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
organization = factories.OrganizationFactory(with_registration_id=True)
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/organizations/{organization.pk!s}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No Organization matches the given query."}
|
||||
|
||||
|
||||
def test_api_organizations_retrieve_authenticated_belong_to_organization(client):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve an organization to which they
|
||||
belong to.
|
||||
"""
|
||||
organization = factories.OrganizationFactory(
|
||||
registration_id_list=["56618615316840", "31561861231231", "98781236231482"],
|
||||
domain_list=["example.com", "example.org"],
|
||||
)
|
||||
user = factories.UserFactory(organization=organization)
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/organizations/{organization.pk!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(organization.pk),
|
||||
"name": organization.name,
|
||||
"abilities": {"delete": False, "get": True, "patch": False, "put": False},
|
||||
"domain_list": ["example.com", "example.org"],
|
||||
"registration_id_list": ["56618615316840", "31561861231231", "98781236231482"],
|
||||
}
|
||||
|
||||
|
||||
def test_api_organizations_retrieve_authenticated_administrator(client):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve an organization
|
||||
which they administrate.
|
||||
"""
|
||||
organization_access = (
|
||||
factories.OrganizationAccessFactory()
|
||||
) # only role is administrator for now
|
||||
user = organization_access.user
|
||||
organization = organization_access.organization
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/organizations/{organization.pk!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["abilities"] == {
|
||||
"delete": False,
|
||||
"get": True,
|
||||
"patch": True,
|
||||
"put": True,
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Tests for Organizations API endpoint in People's core app: update
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_organizations_update_anonymous(client):
|
||||
"""Anonymous users should not be allowed to update an organization."""
|
||||
organization = factories.OrganizationFactory(with_registration_id=True)
|
||||
response = client.patch(
|
||||
f"/api/v1.0/organizations/{organization.pk}/",
|
||||
{"name": "New Name"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_organizations_update_authenticated_unrelated(client):
|
||||
"""
|
||||
Authenticated users should not be allowed to update an organization to which they are
|
||||
not related.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
organization = factories.OrganizationFactory(with_registration_id=True)
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/organizations/{organization.pk}/",
|
||||
{"name": "New Name"},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No Organization matches the given query."}
|
||||
|
||||
|
||||
def test_api_organizations_update_authenticated_belong_to_organization(client):
|
||||
"""
|
||||
Authenticated users should NOT be allowed to update an organization to which they
|
||||
belong to.
|
||||
"""
|
||||
organization = factories.OrganizationFactory(with_registration_id=True)
|
||||
user = factories.UserFactory(organization=organization)
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/organizations/{organization.pk}/",
|
||||
{"name": "New Name"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
def test_api_organizations_update_authenticated_administrator(client):
|
||||
"""
|
||||
Authenticated users should be allowed to update an organization
|
||||
which they administrate.
|
||||
"""
|
||||
organization = factories.OrganizationFactory(
|
||||
registration_id_list=["56618615316840", "31561861231231", "98781236231482"],
|
||||
domain_list=["example.com", "example.org"],
|
||||
)
|
||||
organization_access = factories.OrganizationAccessFactory(organization=organization)
|
||||
user = organization_access.user
|
||||
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/organizations/{organization.pk}/",
|
||||
{"name": "New Name"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(organization.pk),
|
||||
"name": "New Name",
|
||||
"abilities": {"delete": False, "get": True, "patch": True, "put": True},
|
||||
"domain_list": ["example.com", "example.org"],
|
||||
"registration_id_list": ["56618615316840", "31561861231231", "98781236231482"],
|
||||
}
|
||||
@@ -12,6 +12,7 @@ from core.resource_server.urls import urlpatterns as resource_server_urls
|
||||
# - Main endpoints
|
||||
router = DefaultRouter()
|
||||
router.register("contacts", viewsets.ContactViewSet, basename="contacts")
|
||||
router.register("organizations", viewsets.OrganizationViewSet, basename="organizations")
|
||||
router.register("teams", viewsets.TeamViewSet, basename="teams")
|
||||
router.register("users", viewsets.UserViewSet, basename="users")
|
||||
router.register(
|
||||
|
||||
Reference in New Issue
Block a user