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

Feature/24 price endpoint #25

Merged
merged 3 commits into from
Sep 30, 2024
Merged
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
17 changes: 17 additions & 0 deletions src/open_producten/producttypes/admin/price.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
from django.contrib import admin
from django.core.exceptions import ValidationError
from django.forms import BaseInlineFormSet

from ..models import Price, PriceOption


class PriceOptionInlineFormSet(BaseInlineFormSet):

def clean(self):
"""Check that at least one option has been added."""
super().clean()
if any(self.errors):
return
if not any(
cleaned_data and not cleaned_data.get("DELETE", False)
for cleaned_data in self.cleaned_data
):
raise ValidationError("At least one option required.")


class PriceOptionInline(admin.TabularInline):
model = PriceOption
extra = 1
ordering = ("description",)
formset = PriceOptionInlineFormSet


@admin.register(Price)
Expand Down
5 changes: 5 additions & 0 deletions src/open_producten/producttypes/serializers/children.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class Meta:
model = Price
exclude = ("product_type",)

def validate_options(self, options):
if len(options) == 0:
raise serializers.ValidationError("At least one option is required")
return options

@transaction.atomic()
def create(self, validated_data):
options = validated_data.pop("options")
Expand Down
10 changes: 10 additions & 0 deletions src/open_producten/producttypes/serializers/producttype.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,13 @@ def update(self, instance, validated_data):
instance.save()

return instance


class ProductTypeCurrentPriceSerializer(serializers.ModelSerializer):
upl_uri = serializers.ReadOnlyField(source="uniform_product_name.url")
upl_name = serializers.ReadOnlyField(source="uniform_product_name.name")
current_price = PriceSerializer(allow_null=True)

class Meta:
model = ProductType
fields = ("id", "name", "upl_name", "upl_uri", "current_price")
103 changes: 95 additions & 8 deletions src/open_producten/producttypes/tests/api/test_product_type_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,20 @@ def test_read_price_without_credentials_returns_error(self):
response = APIClient().get(self.path)
self.assertEqual(response.status_code, 401)

def test_create_price(self):
def test_create_price_without_options(self):
response = self.post(self.price_data)

self.assertEqual(response.status_code, 201)
self.assertEqual(Price.objects.count(), 1)
self.assertEqual(response.status_code, 400)
self.assertEqual(
self.product_type.prices.first().valid_from, self.price_data["valid_from"]
response.data,
{
"options": [
ErrorDetail(
string="At least one option is required",
code="invalid",
)
]
},
)

def test_create_price_with_price_option(self):
Expand All @@ -69,7 +76,7 @@ def test_create_price_with_price_option(self):
Decimal("74.99"),
)

def test_update_price_removing_options(self):
def test_update_price_removing_all_options(self):
price = self._create_price()
PriceOptionFactory.create(price=price)
PriceOptionFactory.create(price=price)
Expand All @@ -81,9 +88,18 @@ def test_update_price_removing_options(self):

response = self.put(price.id, data)

self.assertEqual(response.status_code, 200)
self.assertEqual(Price.objects.count(), 1)
self.assertEqual(PriceOption.objects.count(), 0)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data,
{
"options": [
ErrorDetail(
string="At least one option is required",
code="invalid",
)
]
},
)

def test_update_price_updating_and_removing_options(self):
price = self._create_price()
Expand Down Expand Up @@ -327,3 +343,74 @@ def test_delete_price(self):
self.assertEqual(response.status_code, 204)
self.assertEqual(Price.objects.count(), 0)
self.assertEqual(PriceOption.objects.count(), 0)

def test_get_current_prices_when_product_type_has_no_prices(self):
response = self.client.get("/api/v1/producttypes/current-prices/")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
[
{
"id": str(self.product_type.id),
"name": self.product_type.name,
"upl_name": self.product_type.uniform_product_name.name,
"upl_uri": self.product_type.uniform_product_name.url,
"current_price": None,
},
],
)

def test_get_current_prices_when_product_type_only_has_price_in_future(self):
PriceFactory.create(
product_type=self.product_type, valid_from=datetime.date(2024, 2, 2)
)

response = self.client.get("/api/v1/producttypes/current-prices/")

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
[
{
"id": str(self.product_type.id),
"name": self.product_type.name,
"upl_name": self.product_type.uniform_product_name.name,
"upl_uri": self.product_type.uniform_product_name.url,
"current_price": None,
},
],
)

def test_get_current_prices_when_product_type_has_current_price(self):
price = PriceFactory.create(
product_type=self.product_type,
valid_from=datetime.date(2024, 1, 1),
)

option = PriceOptionFactory.create(price=price)

response = self.client.get("/api/v1/producttypes/current-prices/")

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
[
{
"id": str(self.product_type.id),
"name": self.product_type.name,
"upl_name": self.product_type.uniform_product_name.name,
"upl_uri": self.product_type.uniform_product_name.url,
"current_price": {
"id": str(price.id),
"valid_from": "2024-01-01",
"options": [
{
"amount": str(option.amount),
"description": option.description,
"id": str(option.id),
}
],
},
},
],
)
21 changes: 17 additions & 4 deletions src/open_producten/producttypes/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from django.shortcuts import get_object_or_404

from rest_framework.decorators import action
from rest_framework.response import Response

from open_producten.producttypes.models import (
Category,
Condition,
Expand All @@ -21,7 +24,10 @@
TagSerializer,
TagTypeSerializer,
)
from open_producten.producttypes.serializers.producttype import ProductTypeSerializer
from open_producten.producttypes.serializers.producttype import (
ProductTypeCurrentPriceSerializer,
ProductTypeSerializer,
)
from open_producten.utils.views import OrderedModelViewSet


Expand All @@ -30,6 +36,16 @@ class ProductTypeViewSet(OrderedModelViewSet):
serializer_class = ProductTypeSerializer
lookup_url_kwarg = "id"

@action(
detail=False,
serializer_class=ProductTypeCurrentPriceSerializer,
url_path="current-prices",
)
def current_prices(self, request):
product_types = ProductType.objects.all()
serializer = ProductTypeCurrentPriceSerializer(product_types, many=True)
return Response(serializer.data)


class ProductTypeChildViewSet(OrderedModelViewSet):

Expand Down Expand Up @@ -107,6 +123,3 @@ class TagTypeViewSet(OrderedModelViewSet):
queryset = TagType.objects.all()
serializer_class = TagTypeSerializer
lookup_field = "id"

def get_queryset(self):
return self.queryset.order_by("id")
Loading
Loading