Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions commcare_connect/opportunity/api/lookups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from rest_framework import serializers, viewsets
from rest_framework.permissions import IsAuthenticated

from commcare_connect.opportunity.models import Country, Currency, DeliveryType


class DeliveryTypeSerializer(serializers.ModelSerializer):
class Meta:
model = DeliveryType
fields = ["id", "name", "slug", "description"]


class CurrencySerializer(serializers.ModelSerializer):
class Meta:
model = Currency
fields = ["code", "name"]


class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ["code", "name", "currency"]


class DeliveryTypeViewSet(viewsets.ReadOnlyModelViewSet):
queryset = DeliveryType.objects.all()
serializer_class = DeliveryTypeSerializer
permission_classes = [IsAuthenticated]
pagination_class = None


class CurrencyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Currency.objects.order_by("code")
serializer_class = CurrencySerializer
permission_classes = [IsAuthenticated]
pagination_class = None


class CountryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Country.objects.order_by("name")
serializer_class = CountrySerializer
permission_classes = [IsAuthenticated]
pagination_class = None
37 changes: 37 additions & 0 deletions commcare_connect/opportunity/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from rest_framework.permissions import IsAuthenticated

from commcare_connect.organization.models import UserOrganizationMembership


class IsOrgProgramManagerAdmin(IsAuthenticated):
"""
Allows access only to authenticated users who are admins of a
program_manager organization. The organization is determined from:
1. URL kwargs ('org_slug')
2. Request data ('organization' field as slug)
3. Request query params ('organization')
"""

def _get_org_slug(self, request, view):
org_slug = view.kwargs.get("org_slug")
if org_slug:
return org_slug
return request.data.get("organization") or request.query_params.get("organization")

def has_permission(self, request, view):
if not super().has_permission(request, view):
return False

org_slug = self._get_org_slug(request, view)
if not org_slug:
return False

try:
membership = UserOrganizationMembership.objects.select_related("organization").get(
user=request.user,
organization__slug=org_slug,
)
except UserOrganizationMembership.DoesNotExist:
return False

return membership.is_admin and membership.organization.program_manager
41 changes: 41 additions & 0 deletions commcare_connect/opportunity/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CompletedModule,
CompletedWork,
CompletedWorkStatus,
DeliverUnit,
LearnModule,
Opportunity,
OpportunityAccess,
Expand Down Expand Up @@ -63,6 +64,34 @@ class Meta:
fields = ["id", "payment_unit_id", "name", "max_total", "max_daily", "amount", "end_date"]


class PaymentUnitCreateSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentUnit
fields = ["name", "description", "amount", "org_amount", "max_total", "max_daily", "start_date", "end_date"]

def create(self, validated_data):
validated_data["opportunity"] = self.context["opportunity"]
return super().create(validated_data)


class DeliverUnitReadSerializer(serializers.ModelSerializer):
class Meta:
model = DeliverUnit
fields = ["id", "slug", "name", "payment_unit", "app", "optional"]


class DeliverUnitCreateSerializer(serializers.ModelSerializer):
class Meta:
model = DeliverUnit
fields = ["slug", "name", "payment_unit", "app", "optional"]

def validate_payment_unit(self, payment_unit):
opportunity = self.context.get("opportunity")
if opportunity and payment_unit.opportunity_id != opportunity.id:
raise serializers.ValidationError("Payment unit does not belong to this opportunity.")
return payment_unit


class OpportunityClaimLimitSerializer(serializers.ModelSerializer):
payment_unit_id = serializers.UUIDField(
source="payment_unit.payment_unit_id",
Expand Down Expand Up @@ -319,3 +348,15 @@ def get_deliveries(self, obj):
.annotate(last_visit_date=Max("uservisit__visit_date"))
)
return CompletedWorkSerializer(completed_works, many=True).data


class InviteUsersSerializer(serializers.Serializer):
phone_numbers = serializers.ListField(child=serializers.CharField())

def validate_phone_numbers(self, phone_numbers):
for number in phone_numbers:
if not number.startswith("+") or not number[1:].isdigit():
raise serializers.ValidationError(
"Phone numbers must contain only digits and include the country code starting with '+'."
)
return phone_numbers
128 changes: 127 additions & 1 deletion commcare_connect/opportunity/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,39 @@
from django.db import transaction
from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from rest_framework import viewsets
from oauth2_provider.contrib.rest_framework.permissions import TokenHasScope
from rest_framework import status, viewsets
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from commcare_connect.flags.switch_names import API_UUID
from commcare_connect.opportunity.api.permissions import IsOrgProgramManagerAdmin
from commcare_connect.opportunity.api.serializers import (
CompletedWorkSerializer,
DeliverUnitCreateSerializer,
DeliverUnitReadSerializer,
DeliveryProgressSerializer,
InviteUsersSerializer,
OpportunitySerializer,
PaymentUnitCreateSerializer,
PaymentUnitSerializer,
UserLearnProgressSerializer,
)
from commcare_connect.opportunity.models import (
CompletedWork,
DeliverUnit,
Opportunity,
OpportunityAccess,
OpportunityClaim,
OpportunityClaimLimit,
Payment,
PaymentUnit,
)
from commcare_connect.opportunity.tasks import add_connect_users
from commcare_connect.users.helpers import create_hq_user_and_link
from commcare_connect.users.models import User
from commcare_connect.utils.db import get_object_or_list_by_uuid_or_int
Expand Down Expand Up @@ -182,3 +193,118 @@ class ConfirmPaymentsView(APIView):

def post(self, request, *args, **kwargs):
return confirm_payments(request, request.user, request.data.get("payments", []))


class ReadSerializerResponseMixin:
"""Return the read serializer in create/update responses instead of the write serializer."""

read_serializer_class = None

def create(self, request, *args, **kwargs):
write_serializer = self.get_serializer(data=request.data)
write_serializer.is_valid(raise_exception=True)
self.perform_create(write_serializer)
read_serializer = self.read_serializer_class(write_serializer.instance, context=self.get_serializer_context())
headers = self.get_success_headers(read_serializer.data)
return Response(read_serializer.data, status=status.HTTP_201_CREATED, headers=headers)


class PaymentUnitViewSet(ReadSerializerResponseMixin, viewsets.ModelViewSet):
serializer_class = PaymentUnitSerializer
read_serializer_class = PaymentUnitSerializer
permission_classes = [IsAuthenticated]
required_scopes = ["create"]
http_method_names = ["get", "post", "patch", "delete", "head", "options"]

def get_permissions(self):
if self.action in ("create", "partial_update", "destroy"):
return [IsAuthenticated(), TokenHasScope(), IsOrgProgramManagerAdmin()]
return [IsAuthenticated()]

def get_serializer_class(self):
if self.action in ("create", "partial_update"):
return PaymentUnitCreateSerializer
return PaymentUnitSerializer

def get_opportunity(self):
return get_object_or_404(Opportunity, opportunity_id=self.kwargs["opportunity_id"])

def get_serializer_context(self):
context = super().get_serializer_context()
if self.kwargs.get("opportunity_id"):
context["opportunity"] = self.get_opportunity()
return context

def get_queryset(self):
user = self.request.user
return (
PaymentUnit.objects.filter(
opportunity__opportunity_id=self.kwargs["opportunity_id"],
)
.filter(
Q(opportunity__organization__memberships__user=user)
| Q(opportunity__managedopportunity__program__organization__memberships__user=user)
)
.distinct()
.order_by("pk")
)


class DeliverUnitViewSet(ReadSerializerResponseMixin, viewsets.ModelViewSet):
serializer_class = DeliverUnitReadSerializer
read_serializer_class = DeliverUnitReadSerializer
permission_classes = [IsAuthenticated]
required_scopes = ["create"]
http_method_names = ["get", "post", "patch", "delete", "head", "options"]

def get_permissions(self):
if self.action in ("create", "partial_update", "destroy"):
return [IsAuthenticated(), TokenHasScope(), IsOrgProgramManagerAdmin()]
return [IsAuthenticated()]

def get_serializer_class(self):
if self.action in ("create", "partial_update"):
return DeliverUnitCreateSerializer
return DeliverUnitReadSerializer

def get_opportunity(self):
return get_object_or_404(Opportunity, opportunity_id=self.kwargs["opportunity_id"])

def get_serializer_context(self):
context = super().get_serializer_context()
if self.kwargs.get("opportunity_id"):
context["opportunity"] = self.get_opportunity()
return context

def get_queryset(self):
"""Filter deliver units by those linked to payment units of this opportunity."""
user = self.request.user
return (
DeliverUnit.objects.filter(
payment_unit__opportunity__opportunity_id=self.kwargs["opportunity_id"],
)
.filter(
Q(payment_unit__opportunity__organization__memberships__user=user)
| Q(payment_unit__opportunity__managedopportunity__program__organization__memberships__user=user)
)
.distinct()
.order_by("pk")
)


class InviteUsersView(APIView):
permission_classes = [IsAuthenticated, TokenHasScope, IsOrgProgramManagerAdmin]
required_scopes = ["create"] # POST-only view, always requires create scope

def post(self, request, opportunity_id):
opportunity = get_object_or_404(Opportunity, opportunity_id=opportunity_id)
if opportunity.has_ended:
return Response(
{"error": "This opportunity has ended. You cannot invite more workers."},
status=400,
)
serializer = InviteUsersSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
phone_numbers = serializer.validated_data["phone_numbers"]
add_connect_users.delay(phone_numbers, str(opportunity.pk))
return Response({"invited": len(phone_numbers)})
Loading
Loading