Skip to content

Commit

Permalink
Merge pull request #4327 from unicef/rest-api-updates-1
Browse files Browse the repository at this point in the history
[217273] REST API updates
  • Loading branch information
pkujawa authored Oct 15, 2024
2 parents 1929252 + a096ad1 commit 3d2e1fd
Show file tree
Hide file tree
Showing 25 changed files with 317 additions and 112 deletions.
10 changes: 10 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ coverage:
- "hct_mis_api/conftest.py"
- "hct_mis_api/config/settings.py"
- "hct_mis_api/apps/core/management/commands/*"
- "pragma: no cover"
- "pragma: no-cover"
- "def __repr__"
- "raise AssertionError"
- "raise NotImplementedError"
- "except ImportError"
- "if __name__ == .__main__."
- "if TYPE_CHECKING"
- "^\\s*(import\\s.+|from\\s+.+import\\s+.+)"
- "logger.exception(e)"
8 changes: 8 additions & 0 deletions src/hct_mis_api/api/endpoints/core/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.core.models import BusinessArea


class BusinessAreaFilter(UpdatedAtFilter):
class Meta:
model = BusinessArea
fields = ("active",)
18 changes: 18 additions & 0 deletions src/hct_mis_api/api/endpoints/core/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
from typing import TYPE_CHECKING, Any

from rest_framework.generics import ListAPIView
from rest_framework.response import Response

from hct_mis_api.api.endpoints.base import HOPEAPIView
from hct_mis_api.api.endpoints.core.filters import BusinessAreaFilter
from hct_mis_api.api.endpoints.core.serializers import BusinessAreaSerializer
from hct_mis_api.apps.core.models import BusinessArea

if TYPE_CHECKING:
from rest_framework.request import Request


class BusinessAreaListView(HOPEAPIView, ListAPIView):
serializer_class = BusinessAreaSerializer
queryset = BusinessArea.objects.all()

def list(self, request: "Request", *args: Any, **kwargs: Any) -> Response:
queryset = self.queryset

filterset = BusinessAreaFilter(request.GET, queryset=queryset)
if filterset.is_valid():
queryset = filterset.qs

page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
3 changes: 2 additions & 1 deletion src/hct_mis_api/api/endpoints/lookups/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from hct_mis_api.api.endpoints.lookups.area import AreaList, AreaTypeList # noqa: F401
from hct_mis_api.api.endpoints.lookups.base import ( # noqa: F401
Country,
CountryAPIView,
DataCollectingPolicy,
DocumentType,
MaritalStatus,
ObservedDisability,
ProgramStatuses,
Relationship,
ResidenceStatus,
Roles,
Expand Down
16 changes: 12 additions & 4 deletions src/hct_mis_api/api/endpoints/lookups/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import TYPE_CHECKING, Any, Optional

from django_countries import Countries
from rest_framework.generics import ListAPIView
from rest_framework.response import Response

from hct_mis_api.api.endpoints.base import HOPEAPIView
from hct_mis_api.api.endpoints.serializers import CountrySerializer
from hct_mis_api.apps.geo.models import Country
from hct_mis_api.apps.household.models import (
COLLECT_TYPES,
IDENTIFICATION_TYPE_CHOICE,
Expand All @@ -25,9 +27,10 @@ def get(self, request: "Request", format: Optional[Any] = None) -> Response:
return Response(dict(IDENTIFICATION_TYPE_CHOICE))


class Country(HOPEAPIView):
def get(self, request: "Request", format: Optional[Any] = None) -> Response:
return Response(dict(Countries()))
class CountryAPIView(HOPEAPIView, ListAPIView):
queryset = Country.objects.all()
serializer_class = CountrySerializer
pagination_class = None


class ResidenceStatus(HOPEAPIView):
Expand Down Expand Up @@ -78,3 +81,8 @@ def get(self, request: "Request", format: Optional[Any] = None) -> Response:
class ProgramScope(HOPEAPIView):
def get(self, request: "Request", format: Optional[Any] = None) -> Response:
return Response(dict(Program.SCOPE_CHOICE))


class ProgramStatuses(HOPEAPIView):
def get(self, request: "Request", format: Optional[Any] = None) -> Response:
return Response(dict(Program.STATUS_CHOICE))
24 changes: 24 additions & 0 deletions src/hct_mis_api/api/endpoints/program/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db.models.query import QuerySet

from django_filters import BooleanFilter, CharFilter

from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.program.models import Program


class ProgramFilter(UpdatedAtFilter):
business_area = CharFilter(field_name="business_area__slug")
active = BooleanFilter(method="is_active_filter")

class Meta:
model = Program
fields = (
"business_area",
"active",
"status",
)

def is_active_filter(self, queryset: "QuerySet[Program]", name: str, value: bool) -> "QuerySet[Program]":
if value is True:
return queryset.filter(status=Program.ACTIVE)
return queryset.exclude(status=Program.ACTIVE)
5 changes: 5 additions & 0 deletions src/hct_mis_api/api/endpoints/program/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter
from rest_framework.generics import ListAPIView

from hct_mis_api.api.endpoints.base import HOPEAPIView
from hct_mis_api.api.endpoints.program.filters import ProgramFilter
from hct_mis_api.api.endpoints.program.serializers import ProgramGlobalSerializer
from hct_mis_api.apps.program.models import Program


class ProgramGlobalListView(HOPEAPIView, ListAPIView):
serializer_class = ProgramGlobalSerializer
queryset = Program.objects.all()
filter_backends = (OrderingFilter, DjangoFilterBackend)
filterset_class = ProgramFilter
18 changes: 12 additions & 6 deletions src/hct_mis_api/api/endpoints/rdi/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from hct_mis_api.api.endpoints.base import HOPEAPIBusinessAreaViewSet
from hct_mis_api.api.models import Grant
from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.program.models import Program

if TYPE_CHECKING:
Expand All @@ -33,17 +34,17 @@ class Meta:


class ProgramViewSet(CreateModelMixin, HOPEAPIBusinessAreaViewSet):
serializer = ProgramSerializer
serializer_class = ProgramSerializer
model = Program
permission = Grant.API_READ_ONLY

def perform_create(self, serializer: "BaseSerializer") -> None:
serializer.save(business_area=self.selected_business_area)
def perform_create(self, serializer_class: "BaseSerializer") -> None:
serializer_class.save(business_area=self.selected_business_area)

@extend_schema(request=ProgramSerializer)
def create(self, request: "Request", *args: Any, **kwargs: Any) -> Response:
self.selected_business_area # TODO does it work? It should be called
serializer = ProgramSerializer(data=request.data)
self.selected_business_area
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
if Grant.API_PROGRAM_CREATE.name not in request.auth.grants:
raise PermissionDenied()
Expand All @@ -53,5 +54,10 @@ def create(self, request: "Request", *args: Any, **kwargs: Any) -> Response:

def list(self, request: "Request", *args: Any, **kwargs: Any) -> Response:
queryset = self.model.objects.filter(business_area=self.selected_business_area)
serializer = self.serializer(queryset, many=True)

filterset = UpdatedAtFilter(request.GET, queryset=queryset)
if filterset.is_valid():
queryset = filterset.qs

serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
10 changes: 10 additions & 0 deletions src/hct_mis_api/api/endpoints/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from django.db import models
from django.utils.translation import gettext_lazy as _

from rest_framework import serializers

from hct_mis_api.apps.geo.models import Country


class RejectPolicy(models.TextChoices):
STRICT = "STRICT", _("STRICT")
LAX = "LAX", _("Lax")


class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ("name", "short_name", "iso_code2", "iso_code3", "iso_num")
3 changes: 2 additions & 1 deletion src/hct_mis_api/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
path("areatypes/", endpoints.lookups.AreaTypeList().as_view(), name="areatype-list"),
path("constance/", ConstanceSettingsAPIView().as_view(), name="constance-list"),
path("lookups/document/", endpoints.lookups.DocumentType().as_view(), name="document-list"),
path("lookups/country/", endpoints.lookups.Country().as_view(), name="country-list"),
path("lookups/country/", endpoints.lookups.CountryAPIView().as_view(), name="country-list"),
path("lookups/residencestatus/", endpoints.lookups.ResidenceStatus().as_view(), name="residencestatus-list"),
path("lookups/maritalstatus/", endpoints.lookups.MaritalStatus().as_view(), name="maritalstatus-list"),
path(
Expand All @@ -36,6 +36,7 @@
),
path("lookups/role/", endpoints.lookups.Roles().as_view(), name="role-list"),
path("lookups/sex/", endpoints.lookups.Sex().as_view(), name="sex-list"),
path("lookups/program-statuses/", endpoints.lookups.ProgramStatuses().as_view(), name="program-statuses-list"),
path("business_areas/", endpoints.core.BusinessAreaListView.as_view(), name="business-area-list"),
path("programs/", ProgramGlobalListView.as_view(), name="program-global-list"),
path(
Expand Down
Empty file.
6 changes: 6 additions & 0 deletions src/hct_mis_api/apps/core/api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django_filters import DateFromToRangeFilter
from django_filters.rest_framework import FilterSet


class UpdatedAtFilter(FilterSet):
updated_at = DateFromToRangeFilter()
3 changes: 2 additions & 1 deletion src/hct_mis_api/apps/geo/api/filters.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django_filters import rest_framework as filters

from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.geo.models import Area


class AreaFilter(filters.FilterSet):
class AreaFilter(UpdatedAtFilter):
level = filters.NumberFilter(
field_name="area_type__area_level",
)
Expand Down
6 changes: 3 additions & 3 deletions src/hct_mis_api/apps/payment/services/payment_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,8 @@ class PaymentGatewayAPIException(Exception):
class PaymentGatewayMissingAPICredentialsException(Exception):
pass

API_EXCEPTION_CLASS = PaymentGatewayAPIException
API_MISSING_CREDENTIALS_EXCEPTION_CLASS = PaymentGatewayMissingAPICredentialsException
API_EXCEPTION_CLASS = PaymentGatewayAPIException # type: ignore
API_MISSING_CREDENTIALS_EXCEPTION_CLASS = PaymentGatewayMissingAPICredentialsException # type: ignore

class Endpoints:
CREATE_PAYMENT_INSTRUCTION = "payment_instructions/"
Expand Down Expand Up @@ -307,7 +307,7 @@ def create_payment_instruction(self, data: dict) -> PaymentInstructionData:

def change_payment_instruction_status(self, status: PaymentInstructionStatus, remote_id: str) -> str:
if status.value not in [s.value for s in PaymentInstructionStatus]:
raise self.API_EXCEPTION_CLASS(f"Can't set invalid Payment Instruction status: {status}")
raise self.API_EXCEPTION_CLASS(f"Can't set invalid Payment Instruction status: {status}") # type: ignore

action_endpoint_map = {
PaymentInstructionStatus.ABORTED: self.Endpoints.ABORT_PAYMENT_INSTRUCTION_STATUS,
Expand Down
11 changes: 8 additions & 3 deletions src/hct_mis_api/apps/periodic_data_update/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.db.models import QuerySet

from constance import config
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
Expand All @@ -22,6 +23,7 @@
PDUUploadPermission,
PDUViewListAndDetailsPermission,
)
from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.core.api.mixins import (
ActionMixin,
BusinessAreaProgramMixin,
Expand Down Expand Up @@ -69,7 +71,8 @@ class PeriodicDataUpdateTemplateViewSet(
"export": [PDUTemplateCreatePermission],
"download": [PDUTemplateDownloadPermission],
}
filter_backends = (OrderingFilter,)
filter_backends = (OrderingFilter, DjangoFilterBackend)
filterset_class = UpdatedAtFilter

def get_queryset(self) -> QuerySet:
business_area = self.get_business_area()
Expand Down Expand Up @@ -132,7 +135,8 @@ class PeriodicDataUpdateUploadViewSet(
"retrieve": [PDUViewListAndDetailsPermission],
"upload": [PDUUploadPermission],
}
filter_backends = (OrderingFilter,)
filter_backends = (OrderingFilter, DjangoFilterBackend)
filterset_class = UpdatedAtFilter

def get_queryset(self) -> QuerySet:
business_area = self.get_business_area()
Expand Down Expand Up @@ -183,7 +187,8 @@ class PeriodicFieldViewSet(
):
serializer_class = PeriodicFieldSerializer
permission_classes = [PDUViewListAndDetailsPermission]
filter_backends = (OrderingFilter,)
filter_backends = (OrderingFilter, DjangoFilterBackend)
filterset_class = UpdatedAtFilter

def get_queryset(self) -> QuerySet:
program = self.get_program()
Expand Down
3 changes: 2 additions & 1 deletion src/hct_mis_api/apps/program/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

from django_filters import rest_framework as filters

from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.core.utils import decode_id_string_required
from hct_mis_api.apps.program.models import ProgramCycle


class ProgramCycleFilter(filters.FilterSet):
class ProgramCycleFilter(UpdatedAtFilter):
search = filters.CharFilter(method="search_filter")
status = filters.MultipleChoiceFilter(
choices=ProgramCycle.STATUS_CHOICE,
Expand Down
3 changes: 2 additions & 1 deletion src/hct_mis_api/apps/registration_data/api/filters.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django_filters import rest_framework as filters

from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.registration_data.models import RegistrationDataImport


class RegistrationDataImportFilter(filters.FilterSet):
class RegistrationDataImportFilter(UpdatedAtFilter):
name = filters.CharFilter(
field_name="name",
lookup_expr="startswith",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class DeduplicationEngineAPIException(Exception):
class DeduplicationEngineMissingAPICredentialsException(Exception):
pass

API_EXCEPTION_CLASS = DeduplicationEngineAPIException
API_MISSING_CREDENTIALS_EXCEPTION_CLASS = DeduplicationEngineMissingAPICredentialsException
API_EXCEPTION_CLASS = DeduplicationEngineAPIException # type: ignore
API_MISSING_CREDENTIALS_EXCEPTION_CLASS = DeduplicationEngineMissingAPICredentialsException # type: ignore

class Endpoints:
GET_DEDUPLICATION_SETS = "deduplication_sets/" # GET - List view
Expand Down
3 changes: 2 additions & 1 deletion src/hct_mis_api/apps/targeting/api/filters.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django_filters import rest_framework as filters

from hct_mis_api.apps.core.api.filters import UpdatedAtFilter
from hct_mis_api.apps.targeting.models import TargetPopulation


class TargetPopulationFilter(filters.FilterSet):
class TargetPopulationFilter(UpdatedAtFilter):
name = filters.CharFilter(
field_name="name",
lookup_expr="startswith",
Expand Down
3 changes: 2 additions & 1 deletion src/hct_mis_api/config/fragments/drf.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 10,
"PAGE_SIZE": 50,
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"DEFAULT_AUTHENTICATION_CLASSES": [
"hct_mis_api.api.utils.CsrfExemptSessionAuthentication",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"SECURE_SSL_REDIRECT": True,
}
1 change: 0 additions & 1 deletion tests/.coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ omit =
hct_mis_api/conftest.py
hct_mis_api/config/settings.py
hct_mis_api/apps/core/management/commands/*
hct_mis_api/apps/household/forms.py

[report]
# Regexes for lines to exclude from consideration
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/api/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import contextlib
from typing import Iterator

from django.urls import reverse

from rest_framework import status
Expand All @@ -13,6 +16,16 @@
from tests.unit.api.factories import APITokenFactory


@contextlib.contextmanager
def token_grant_permission(token: APIToken, grant: Grant) -> Iterator:
old = token.grants
token.grants += [grant.name]
token.save()
yield
token.grants = old
token.save()


class HOPEApiTestCase(APITestCase):
databases = {"default"}
user_permissions = [
Expand Down
Loading

0 comments on commit 3d2e1fd

Please sign in to comment.