Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rlecellier/request response serializers #235

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to
- Allow on-demand page size on the order and enrollment endpoints
- Add yarn cli to generate joanie api client in TypeScript
- Display course runs into the admin course change view
- Add course query param to openapi schema on route products.retrieve

### Removed

Expand Down
76 changes: 64 additions & 12 deletions src/backend/joanie/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _

from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins, pagination, permissions, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError as DRFValidationError
Expand All @@ -14,6 +16,10 @@

from joanie.core import models
from joanie.core.enums import ORDER_STATE_PENDING
from joanie.core.viewsets import (
RequestResponseSerializersViewSetMixin,
ActionSerializerType,
)
from joanie.payment import get_payment_backend
from joanie.payment.models import Invoice

Expand Down Expand Up @@ -111,6 +117,12 @@ def get_serializer_context(self):

return context

@swagger_auto_schema(
query_serializer=serializers.ProductRetrieveQuerySerializer,
)
def retrieve(self, *args, **kwargs):
return super().retrieve(*args, **kwargs)


# pylint: disable=too-many-ancestors
class EnrollmentViewSet(
Expand Down Expand Up @@ -141,6 +153,7 @@ def perform_create(self, serializer):

# pylint: disable=too-many-ancestors
class OrderViewSet(
RequestResponseSerializersViewSetMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
Expand All @@ -163,6 +176,12 @@ class OrderViewSet(
pagination_class = Pagination
permission_classes = [permissions.IsAuthenticated]
serializer_class = serializers.OrderSerializer
action_serializers = {
"create": {
"request": serializers.OrderCreateSerializer,
"response": serializers.OrderCreateResponseSerializer,
}
}
filterset_class = filters.OrderViewSetFilter
ordering = ["-created_on"]

Expand All @@ -171,21 +190,22 @@ def get_queryset(self):
user = User.update_or_create_from_request_user(request_user=self.request.user)
return user.orders.all().select_related("owner", "product", "certificate")

def perform_create(self, serializer):
def perform_create(self, validated_data):
"""Force the order's "owner" field to the logged-in user."""
owner = User.update_or_create_from_request_user(request_user=self.request.user)
serializer.save(owner=owner)
return models.Order.objects.create(**validated_data, owner=owner)

@transaction.atomic
def create(self, request, *args, **kwargs):
"""Try to create an order and a related payment if the payment is fee."""
serializer = self.get_serializer(data=request.data)
serializer = self.get_request_serializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=400)

product = serializer.validated_data.get("product")
course = serializer.validated_data.get("course")
billing_address = serializer.initial_data.get("billing_address")
billing_address = serializer.validated_data.get("billing_address")
credit_card_id = serializer.validated_data.get("credit_card_id")

# Populate organization field if it is not set and there is only one
# on the product
Expand All @@ -210,7 +230,13 @@ def create(self, request, *args, **kwargs):

# - Validate data then create an order
try:
self.perform_create(serializer)
order_validated_data = {**serializer.validated_data}
if billing_address:
order_validated_data.pop("billing_address")
if credit_card_id:
order_validated_data.pop("credit_card_id")
# FIXME this pop stuff should be done in OrderCreateSerializer.save
order = self.perform_create(order_validated_data)
except (DRFValidationError, IntegrityError):
return Response(
(
Expand All @@ -222,10 +248,7 @@ def create(self, request, *args, **kwargs):

# Once order has been created, if product is not free, create a payment
if product.price.amount > 0:
order = serializer.instance
payment_backend = get_payment_backend()
credit_card_id = serializer.initial_data.get("credit_card_id")

# if payment in one click
if credit_card_id:
try:
Expand All @@ -245,14 +268,22 @@ def create(self, request, *args, **kwargs):
request=request, order=order, billing_address=billing_address
)

# Return the fresh new order with payment_info
return Response(
{**serializer.data, "payment_info": payment_info}, status=201
response_serializer = self.get_response_serializer(
instance=order,
context={
"payment_info": payment_info,
},
)
return Response(response_serializer.data, status=201)

# Else return the fresh new order
return Response(serializer.data, status=201)
response_serializer = self.get_response_serializer(instance=order)
return Response(response_serializer.data, status=201)

@swagger_auto_schema(
request_body=serializers.OrderAbortBodySerializer,
responses={204: serializers.EmptyResponseSerializer},
)
@action(detail=True, methods=["POST"])
def abort(self, request, pk=None): # pylint: disable=no-self-use, invalid-name
"""Abort a pending order and the related payment if there is one."""
Expand All @@ -277,6 +308,17 @@ def abort(self, request, pk=None): # pylint: disable=no-self-use, invalid-name

return Response(status=204)

@swagger_auto_schema(
query_serializer=serializers.OrderInvoiceQuerySerializer,
responses={
200: openapi.Response(
"File Attachment", schema=openapi.Schema(type=openapi.TYPE_FILE)
),
400: serializers.ErrorResponseSerializer,
404: serializers.ErrorResponseSerializer,
},
produces="application/pdf",
)
@action(detail=True, methods=["GET"])
def invoice(self, request, pk=None): # pylint: disable=no-self-use, invalid-name
"""
Expand Down Expand Up @@ -391,6 +433,16 @@ def get_queryset(self):
user = User.update_or_create_from_request_user(request_user=self.request.user)
return models.Certificate.objects.filter(order__owner=user)

@swagger_auto_schema(
responses={
200: openapi.Response(
"File Attachment", schema=openapi.Schema(type=openapi.TYPE_FILE)
),
404: serializers.ErrorResponseSerializer,
422: serializers.ErrorResponseSerializer,
},
produces="application/pdf",
)
@action(detail=True, methods=["GET"])
def download(self, request, pk=None): # pylint: disable=no-self-use, invalid-name
"""
Expand Down
80 changes: 80 additions & 0 deletions src/backend/joanie/core/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from drf_yasg.inspectors import SwaggerAutoSchema
import drf_yasg.inspectors.base
import drf_yasg.openapi
import drf_yasg.utils

from joanie.core.viewsets import ActionSerializerType


def _call_view_method(
view, method_name, fallback_attr=None, default=None, args=None, kwargs=None
):
"""Override of drf_yasg.inspectors.base.call_view_method to allow passing args."""
if hasattr(view, method_name):
try:
view_method, is_callabale = drf_yasg.inspectors.base.is_callable_method(
view, method_name
)
if is_callabale:
args = args or []
kwargs = kwargs or {}
return view_method(*args, **kwargs)
except Exception: # pragma: no cover
drf_yasg.inspectors.base.logger.warning(
"view's %s raised exception during schema generation; use "
"`getattr(self, 'swagger_fake_view', False)` to detect and short-circuit this",
type(view).__name__,
exc_info=True,
)

if fallback_attr and hasattr(view, fallback_attr):
return getattr(view, fallback_attr)

return default


class CustomAutoSchema(SwaggerAutoSchema):
"""
SwaggerAutoSchema for viewsets with Request and Response serializers.
https://github.com/axnsan12/drf-yasg/blob/master/src/drf_yasg/inspectors/view.py
"""

def get_view_serializer(self, serializer_type):
"""Retrieve the serializer type"""
return _call_view_method(
self.view,
"get_serializer",
kwargs={"context": {"serializer_type": serializer_type}},
)

def get_request_serializer(self):
"""Retrieve Request serializer"""
body_override = self._get_request_body_override()

if body_override is None and self.method in self.implicit_body_methods:
return _call_view_method(
self.view,
"get_serializer",
kwargs={
"context": {"serializer_type": ActionSerializerType.REQUEST.value}
},
)

if body_override is drf_yasg.utils.no_body:
return None

return body_override

def get_default_response_serializer(self):
"""Retrieve Redsponse serializer"""
body_override = self._get_request_body_override()
if body_override and body_override is not drf_yasg.utils.no_body:
return body_override

return _call_view_method(
self.view,
"get_serializer",
kwargs={
"context": {"serializer_type": ActionSerializerType.RESPONSE.value}
},
)
7 changes: 7 additions & 0 deletions src/backend/joanie/core/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .model_serializers import *
from .empty_response_serializer import *
from .error_response_serializer import *
from .order_create_body_serializer import *
from .order_abort_body_serializer import *
from .order_invoice_query_serializer import *
from .product_retrieve_query_serializer import *
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Serializers for empty Response"""

from rest_framework import serializers


class EmptyResponseSerializer(serializers.Serializer):
pass
10 changes: 10 additions & 0 deletions src/backend/joanie/core/serializers/error_response_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Serializers for core.api.OrderViewSet.abort Body"""

from rest_framework import serializers


class ErrorResponseSerializer(serializers.Serializer):
details = serializers.CharField(required=True)

class Meta:
fields = ["details"]
Loading