server/store/views.py (141 lines of code) (raw):
#!/usr/bin/python
#
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.views.decorators.csrf import ensure_csrf_cookie
from django.middleware.csrf import get_token
from django.views.decorators.http import require_http_methods
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from store.models import Product, SiteConfig, Testimonial, Transaction
from store.serializers import (
ProductSerializer,
SiteConfigSerializer,
TestimonialSerializer,
CartSerializer,
CheckoutSerializer,
)
class ProductPurchaseException(APIException):
status_code = 405
default_detail = {
"code": status_code,
"message": "Unable to complete purchase - no inventory",
}
def log_error(error_name, error_message, product):
# Log error by writing structured JSON. Can be then used with log-based alerting, metrics, etc.
print(
json.dumps(
{
"severity": "ERROR",
"error": error_name,
"message": f"{error_name}: {error_message}",
"method": "ProductViewSet.purchase()",
"product": product.id,
"countRequested": 1,
"countFulfilled": product.inventory_count,
}
)
)
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
class ProductPurchaseException(APIException):
status_code = 405
default_detail = {
"code": status_code,
"message": "Unable to complete purchase - no inventory",
}
@action(detail=True, methods=["get", "post"])
def purchase(self, request, pk):
product = get_object_or_404(Product, id=pk)
if product.inventory_count > 0:
product.inventory_count -= 1
product.save()
Transaction.objects.create(
datetime=timezone.now(), product_id=product, unit_price=product.price
)
else:
log_error(
"INVENTORY_COUNT_ERROR",
"A purchase was attempted where there was insufficient inventory to fulfil the order.",
product,
)
raise ProductPurchaseException()
# If the transaction caused a product to sell out, log an error
if product.inventory_count == 0:
log_error(
"INVENTORY_SOLDOUT_ERROR",
"A purchase just caused a product to sell out. More inventory will be required.",
product,
)
serializer = ProductSerializer(product)
return Response(serializer.data)
class ActiveProductViewSet(viewsets.ViewSet):
@extend_schema(request=None, responses=ProductSerializer)
def list(self, request, formatting=None):
active_product = get_object_or_404(Product, active=True)
serializer = ProductSerializer(active_product, context={"request": request})
return Response(serializer.data)
class TestimonialViewSet(viewsets.ModelViewSet):
queryset = Testimonial.objects.order_by("-rating").all()
serializer_class = TestimonialSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ["product_id"]
class SiteConfigViewSet(viewsets.ModelViewSet):
queryset = SiteConfig.objects.all()
serializer_class = SiteConfigSerializer
class ActiveSiteConfigViewSet(viewsets.ViewSet):
@extend_schema(responses=SiteConfigSerializer)
def list(self, request, formatting=None):
active = get_object_or_404(SiteConfig, active=True)
serializer = SiteConfigSerializer(active)
return Response(serializer.data)
@ensure_csrf_cookie
@require_http_methods(["POST"])
def checkout(request):
def lift_item_status(data):
status = ""
for item in data["items"]:
if "status" in item:
for i in item["status"]:
status = str(i)
return status
serializer = CartSerializer(data=json.loads(request.body))
if not serializer.is_valid():
status_code = 400
status = "validation_error"
if "payment" in serializer.errors:
status_code = 501
status = serializer.errors["payment"]["method"][0].code
if "items" in serializer.errors:
status = lift_item_status(serializer.errors)
return JsonResponse(
{"status": status, "errors": serializer.errors}, status=status_code
)
cart = serializer.validated_data
items = []
for item in cart["items"]:
product = get_object_or_404(Product, id=item["id"])
count = item["countRequested"]
product.inventory_count -= count
product.save()
for _ in range(count):
Transaction.objects.create(
datetime=timezone.now(), product_id=product, unit_price=product.price
)
items.append(
{"id": product.id, "countRequested": count, "countFulfilled": count}
)
if product.inventory_count == 0:
log_error(
"INVENTORY_SOLDOUT_ERROR",
"A purchase just caused a product to sell out. More inventory will be required.",
product,
)
response = CheckoutSerializer(data={"status": "complete", "items": items})
response.is_valid()
return JsonResponse(response.data)
def csrf_token(request):
return JsonResponse({"csrfToken": get_token(request)})