diff --git a/greg/api/serializers/organizational_unit.py b/greg/api/serializers/organizational_unit.py index 3e9420ae0ac75fc6c6f3050fbcaf86f901111425..3c4acd7cd32f409376a9faa54ae012e03c48242a 100644 --- a/greg/api/serializers/organizational_unit.py +++ b/greg/api/serializers/organizational_unit.py @@ -1,8 +1,8 @@ -from rest_framework.serializers import ModelSerializer, CharField +from rest_framework.serializers import ModelSerializer from greg.api.serializers.ouidentifier import OuIdentifierSerializer -from greg.models import OrganizationalUnit +from greg.models import OrganizationalUnit, SponsorOrganizationalUnit class OrganizationalUnitSerializer(ModelSerializer): @@ -24,19 +24,14 @@ class OrganizationalUnitSerializer(ModelSerializer): class SponsorOrgUnitsSerializer(ModelSerializer): - nb = CharField(source="name_nb") - en = CharField(source="name_en") - class Meta: - model = OrganizationalUnit + model = SponsorOrganizationalUnit fields = [ "id", - "nb", - "en", - "orgreg_id", - "identifier_1", - "identifier_2", - "acronym_nob", - "acronym_nno", - "acronym_eng", + "sponsor", + "organizational_unit", + "hierarchical_access", + "automatic", + "source", ] + read_only_fields = ["sponsor", "organizational_unit"] diff --git a/greg/api/urls.py b/greg/api/urls.py index 492f3a22e8e6a46ed3739c321d3452010f8c43bf..2e721797ed19d75ac9d684b1e0747c1dc5d5b965 100644 --- a/greg/api/urls.py +++ b/greg/api/urls.py @@ -72,9 +72,16 @@ urlpatterns += [ SponsorGuestsViewSet.as_view({"get": "list"}), name="sponsor_guests-list", ), + re_path( + r"^sponsors/(?P<sponsor_id>[0-9]+)/orgunits$", + SponsorOrgunitLinkView.as_view({"get": "list"}), + name="sponsor_orgunit-list", + ), re_path( r"^sponsors/(?P<sponsor_id>[0-9]+)/orgunit/(?P<orgunit_id>[0-9]+)$", - SponsorOrgunitLinkView.as_view({"post": "create", "delete": "destroy"}), + SponsorOrgunitLinkView.as_view( + {"post": "create", "delete": "destroy", "get": "retrieve"} + ), name="sponsor_orgunit-detail", ), re_path( diff --git a/greg/api/views/sponsor.py b/greg/api/views/sponsor.py index 8c7a337f6c1ecdd17fb49ee5631bf5efc94170b1..755ee5283af533331134252d39a8cffb9ec3c5f1 100644 --- a/greg/api/views/sponsor.py +++ b/greg/api/views/sponsor.py @@ -9,9 +9,9 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet from greg.api.pagination import PrimaryKeyCursorPagination from greg.api.serializers import PersonSerializer -from greg.api.serializers.organizational_unit import OrganizationalUnitSerializer +from greg.api.serializers.organizational_unit import SponsorOrgUnitsSerializer from greg.api.serializers.sponsor import SponsorSerializer -from greg.models import Sponsor, Person, OrganizationalUnit +from greg.models import Sponsor, Person, OrganizationalUnit, SponsorOrganizationalUnit logger = logging.getLogger(__name__) @@ -73,8 +73,8 @@ class SponsorOrgunitLinkView( ): """Endpoint that allows manipulation of links between a sponsor and the units he is attached to""" - queryset = OrganizationalUnit.objects.all().order_by("id") - serializer_class = OrganizationalUnitSerializer + queryset = SponsorOrganizationalUnit.objects.all().order_by("id") + serializer_class = SponsorOrgUnitsSerializer pagination_class = PrimaryKeyCursorPagination permission_classes = (permissions.IsAdminUser,) # This is set so that the orgunit_id parameter in the path of the URL is used for looking up objects @@ -82,14 +82,36 @@ class SponsorOrgunitLinkView( def create(self, request, *args, **kwargs): (sponsor_id, orgunit_id) = self._extract_sponsor_and_orgunit(kwargs) - # Default to false if hierarchical_access is not specified + sponsor_orgunit = self.queryset.filter( + sponsor__id=sponsor_id, organizational_unit__id=orgunit_id + ) + # Set default values if fields are not given hierarchical_access = request.data.get("hierarchical_access", "False") - sponsor = Sponsor.objects.get(id=sponsor_id) - sponsor.units.add( - orgunit_id, through_defaults={"hierarchical_access": hierarchical_access} + source = request.data.get("source", "") + automatic = request.data.get("automatic", "False") + + if sponsor_orgunit: + sponsor_orgunit.update( + hierarchical_access=hierarchical_access, + automatic=automatic, + source=source, + ) + serializer = self.serializer_class(sponsor_orgunit, many=True) + return Response(serializer.data) + + SponsorOrganizationalUnit.objects.create( + sponsor=Sponsor.objects.get(id=kwargs["sponsor_id"]), + organizational_unit=OrganizationalUnit.objects.get(id=kwargs["orgunit_id"]), + hierarchical_access=hierarchical_access, + automatic=automatic, + source=source, ) - return Response(status=status.HTTP_204_NO_CONTENT) + sponsor_orgunit = self.queryset.filter( + sponsor__id=sponsor_id, organizational_unit__id=orgunit_id + ) + serializer = self.serializer_class(sponsor_orgunit, many=True) + return Response(serializer.data) def destroy(self, request, *args, **kwargs): """Overridden because a delete at this endpoint should not attempt to delete the organizational unit, @@ -100,6 +122,22 @@ class SponsorOrgunitLinkView( return Response(status=status.HTTP_204_NO_CONTENT) + def list(self, request, *args, **kwargs): + """Lists all SponsorOrganizationalUnit objects connected to the sponsor""" + sponsor_id = kwargs["sponsor_id"] + all_units = self.queryset.filter(sponsor__id=sponsor_id).all() + serializer = self.serializer_class(all_units, many=True) + return Response(serializer.data) + + def retrieve(self, request, *args, **kwargs): + """Returns a given SponsorOrganizationalUnit object""" + (sponsor_id, orgunit_id) = self._extract_sponsor_and_orgunit(kwargs) + unit = self.queryset.filter( + sponsor__id=sponsor_id, organizational_unit__id=orgunit_id + ) + serializer = self.serializer_class(unit, many=True) + return Response(serializer.data) + def _extract_sponsor_and_orgunit(self, request_data): sponsor_id = request_data["sponsor_id"] orgunit_id = request_data["orgunit_id"] diff --git a/greg/tests/api/test_sponsor.py b/greg/tests/api/test_sponsor.py index 5b48fc5a686b3d77d88a5092650114a9ed576669..c0ae4d00c9088aa4a343a8050d8d24935188cc14 100644 --- a/greg/tests/api/test_sponsor.py +++ b/greg/tests/api/test_sponsor.py @@ -81,7 +81,7 @@ def test_add_sponsor_with_unit(client, unit_foo: OrganizationalUnit): kwargs={"sponsor_id": sponsor_id, "orgunit_id": unit_foo.id}, ) response = client.post(create_sponsor_link_url, data=data) - assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.status_code == status.HTTP_200_OK sponsor_lookup_response = client.get(sponsor_url, kwargs={"id": sponsor_id}) sponsor_lookup_response_body = sponsor_lookup_response.json() @@ -106,7 +106,7 @@ def test_remove_sponsor_orgunit_link( kwargs={"sponsor_id": sponsor_guy.id, "orgunit_id": unit_foo.id}, ) response = client.post(sponsor_orgunit_url, data=data) - assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.status_code == status.HTTP_200_OK response_get = client.get(sponsor_detail_url).json() @@ -174,7 +174,9 @@ def test_add_sponsor_unit_link_with_no_access_parameter( kwargs={"sponsor_id": sponsor_id, "orgunit_id": unit_foo.id}, ) response = client.post(create_sponsor_link_url) - assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.status_code == status.HTTP_200_OK + response_body = response.json() + assert len(response_body) == 1 # Check that the unit is attached to the sponsor sponsor_lookup_response = client.get(sponsor_url, kwargs={"id": sponsor_id}) @@ -189,3 +191,104 @@ def test_add_sponsor_unit_link_with_no_access_parameter( sponsor_id=sponsor_id, organizational_unit_id=unit_foo.id ).get() assert not sponsor_organization_unit.hierarchical_access + + +@pytest.mark.django_db +def test_retrieve_sponsor_orgunit( + client, + sponsor_guy: Sponsor, + unit_foo: OrganizationalUnit, + sponsor_org_unit: SponsorOrganizationalUnit, +): + url = reverse( + "v1:sponsor_orgunit-detail", + kwargs={"sponsor_id": sponsor_guy.id, "orgunit_id": unit_foo.id}, + ) + sponsor_unit = client.get(url).json() + + assert len(sponsor_unit) == 1 + + +@pytest.mark.django_db +def test_no_sponsor_orgunit_exists( + client, + sponsor_foo: Sponsor, + unit_foo: OrganizationalUnit, +): + url = reverse( + "v1:sponsor_orgunit-detail", + kwargs={"sponsor_id": sponsor_foo.id, "orgunit_id": unit_foo.id}, + ) + sponsor_unit = client.get(url).json() + + assert len(sponsor_unit) == 0 + + +@pytest.mark.django_db +def test_list_all_sponsor_orgunits( + client, + sponsor_guy: Sponsor, + sponsor_org_unit: SponsorOrganizationalUnit, + sponsor_org_unit_guy: SponsorOrganizationalUnit, +): + url = reverse("v1:sponsor_orgunit-list", kwargs={"sponsor_id": sponsor_guy.id}) + + sponsor_units = client.get(url).json() + + assert len(sponsor_units) == 2 + + +@pytest.mark.django_db +def test_update_sponsor_orgunit( + client, + sponsor_guy: Sponsor, + unit_foo: OrganizationalUnit, + sponsor_org_unit: SponsorOrganizationalUnit, +): + data = { + "hierarchical_access": True, + "automatic": False, + "source": "test", + } + url = reverse( + "v1:sponsor_orgunit-detail", + kwargs={"sponsor_id": sponsor_guy.id, "orgunit_id": unit_foo.id}, + ) + assert sponsor_org_unit.hierarchical_access is False + response_data = client.post(url, data=data).json()[0] + + assert response_data["hierarchical_access"] == data["hierarchical_access"] + assert response_data["automatic"] == data["automatic"] + assert response_data["source"] == data["source"] + + +@pytest.mark.django_db +def test_create_new_sponsor_orgunit( + client, + sponsor_foo: Sponsor, + unit_foo: OrganizationalUnit, +): + data = { + "hierarchical_access": True, + "automatic": False, + "source": "test", + } + get_url = reverse( + "v1:sponsor_orgunit-detail", + kwargs={"sponsor_id": sponsor_foo.id, "orgunit_id": unit_foo.id}, + ) + sponsor_unit = client.get(get_url).json() + assert len(sponsor_unit) == 0 + + post_url = reverse( + "v1:sponsor_orgunit-detail", + kwargs={"sponsor_id": sponsor_foo.id, "orgunit_id": unit_foo.id}, + ) + + response_data = client.post(post_url, data=data).json() + assert len(response_data) == 1 + + response_data = response_data[0] + assert response_data["hierarchical_access"] == data["hierarchical_access"] + assert response_data["automatic"] == data["automatic"] + assert response_data["source"] == data["source"] diff --git a/greg/tests/conftest.py b/greg/tests/conftest.py index 1ceabf4b1b0e432c9cccc1bd1a8dd39c69bb176d..22f0ddff674a7c86721619384d01767d38006152 100644 --- a/greg/tests/conftest.py +++ b/greg/tests/conftest.py @@ -124,6 +124,17 @@ def sponsor_guy() -> Sponsor: return Sponsor.objects.get(id=sp.id) +@pytest.fixture +def sponsor_foo() -> Sponsor: + sp = Sponsor.objects.create( + feide_id="foo@example.org", + first_name="Sponsor", + last_name="Foo", + work_email="sponsor_foo@example.com", + ) + return Sponsor.objects.get(id=sp.id) + + @pytest.fixture def person_foo_verified(person_foo, sponsor_guy) -> Identity: pi = Identity.objects.create( @@ -281,6 +292,14 @@ def sponsor_org_unit(sponsor_guy, unit_foo): return SponsorOrganizationalUnit.objects.get(id=1) +@pytest.fixture +def sponsor_org_unit_guy(sponsor_guy, unit_foo2): + SponsorOrganizationalUnit.objects.create( + sponsor=sponsor_guy, organizational_unit=unit_foo2, hierarchical_access=False + ) + return SponsorOrganizationalUnit.objects.get(id=2) + + @pytest.fixture def notification(): Notification.objects.create( diff --git a/gregui/api/views/ou.py b/gregui/api/views/ou.py index a7dbe1e7c238c7776b79531b21e19a5b7989cd05..5081f8d40c7e35a05907fae8ff8debb54ac3483b 100644 --- a/gregui/api/views/ou.py +++ b/gregui/api/views/ou.py @@ -1,19 +1,39 @@ from rest_framework import mixins from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.permissions import IsAuthenticated +from rest_framework.serializers import CharField, ModelSerializer from rest_framework.viewsets import GenericViewSet -from greg.api.serializers.organizational_unit import SponsorOrgUnitsSerializer +from greg.models import OrganizationalUnit from greg.permissions import IsSponsor from gregui.models import GregUserProfile +class OrgUnitsSerializer(ModelSerializer): + nb = CharField(source="name_nb") + en = CharField(source="name_en") + + class Meta: + model = OrganizationalUnit + fields = [ + "id", + "nb", + "en", + "orgreg_id", + "identifier_1", + "identifier_2", + "acronym_nob", + "acronym_nno", + "acronym_eng", + ] + + class OusViewSet(mixins.ListModelMixin, GenericViewSet): """Fetch Ous related to the authenticated sponsor.""" authentication_classes = [SessionAuthentication, BasicAuthentication] permission_classes = [IsAuthenticated, IsSponsor] - serializer_class = SponsorOrgUnitsSerializer + serializer_class = OrgUnitsSerializer def get_queryset(self): sponsor = GregUserProfile.objects.get(user=self.request.user).sponsor