Skip to content

Commit

Permalink
refactor producten api & tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Floris272 committed Jan 2, 2025
1 parent 445a6bd commit a29bdbd
Show file tree
Hide file tree
Showing 7 changed files with 518 additions and 101 deletions.
4 changes: 2 additions & 2 deletions src/open_producten/producten/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.16 on 2024-11-29 13:40
# Generated by Django 4.2.17 on 2024-12-27 19:19

import django.core.validators
from django.db import migrations, models
Expand All @@ -12,7 +12,7 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
("producttypen", "0002_producttype_contacten_producttype_locaties_and_more"),
("producttypen", "0004_producttype_contacten_producttype_locaties_and_more"),
]

operations = [
Expand Down
10 changes: 6 additions & 4 deletions src/open_producten/producten/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ class Meta:
verbose_name_plural = _("Producten")

def clean(self):
if not self.bsn and not self.kvk:
raise ValidationError(
"Een product moet een bsn, kvk nummer of beiden hebben."
)
validate_bsn_or_kvk(self.bsn, self.kvk)

def __str__(self):
return f"{self.bsn if self.bsn else self.kvk} {self.product_type.naam}"


def validate_bsn_or_kvk(bsn, kvk):
if not bsn and not kvk:
raise ValidationError("Een product moet een bsn, kvk nummer of beiden hebben.")
37 changes: 5 additions & 32 deletions src/open_producten/producten/serializers/product.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,18 @@
from django.core.validators import ValidationError

from rest_framework import serializers

from open_producten.producten.models import Product
from open_producten.producten.serializers.validators import BsnOrKvkValidator
from open_producten.producttypen.models import ProductType
from open_producten.producttypen.serializers.onderwerp import (
SimpleProductTypeSerializer,
)
from open_producten.utils.serializers import model_to_dict_with_related_ids


class BaseProductSerializer(serializers.ModelSerializer):
def validate(self, attrs):

if self.partial:
all_attrs = model_to_dict_with_related_ids(self.instance) | attrs
else:
all_attrs = attrs

instance = Product(**all_attrs)

try:
instance.clean()
except ValidationError as e:
raise serializers.ValidationError({"bsn_or_kvk": e.message})
from open_producten.producttypen.serializers.thema import NestedProductTypeSerializer

return attrs


class ProductSerializer(BaseProductSerializer):
product_type = SimpleProductTypeSerializer(read_only=True)
class ProductSerializer(serializers.ModelSerializer):
product_type = NestedProductTypeSerializer(read_only=True)
product_type_id = serializers.PrimaryKeyRelatedField(
write_only=True, queryset=ProductType.objects.all(), source="product_type"
)

class Meta:
model = Product
fields = "__all__"


class ProductUpdateSerializer(BaseProductSerializer):
class Meta:
model = Product
exclude = ("product_type",)
validators = [BsnOrKvkValidator()]
18 changes: 18 additions & 0 deletions src/open_producten/producten/serializers/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.core.exceptions import ValidationError

from rest_framework import serializers

from open_producten.producten.models.product import validate_bsn_or_kvk
from open_producten.utils.serializers import get_from_serializer_data_or_instance


class BsnOrKvkValidator:
requires_context = True

def __call__(self, value, serializer):
bsn = get_from_serializer_data_or_instance("bsn", value, serializer)
kvk = get_from_serializer_data_or_instance("kvk", value, serializer)
try:
validate_bsn_or_kvk(bsn, kvk)
except ValidationError as e:
raise serializers.ValidationError({"bsn_or_kvk": e.message})
192 changes: 138 additions & 54 deletions src/open_producten/producten/tests/api/test_product.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,16 @@
import datetime

from django.urls import reverse

from freezegun import freeze_time
from rest_framework import status
from rest_framework.exceptions import ErrorDetail
from rest_framework.test import APIClient

from open_producten.producten.models import Product
from open_producten.producten.tests.factories import ProductFactory
from open_producten.producttypen.tests.factories import ProductTypeFactory
from open_producten.utils.tests.cases import BaseApiTestCase
from open_producten.utils.tests.helpers import model_to_dict_with_id


def product_to_dict(product):
product_dict = model_to_dict_with_id(product)
product_dict["start_datum"] = str(product_dict["start_datum"])
product_dict["eind_datum"] = str(product_dict["eind_datum"])
product_dict["aanmaak_datum"] = str(product.aanmaak_datum.astimezone().isoformat())
product_dict["update_datum"] = str(product.update_datum.astimezone().isoformat())

product_dict["product_type"] = model_to_dict_with_id(
product.product_type,
exclude=(
"onderwerpen",
"contacten",
"locaties",
"organisaties",
),
)
product_dict["product_type"][
"uniforme_product_naam"
] = product.product_type.uniforme_product_naam.uri

product_dict["product_type"]["aanmaak_datum"] = str(
product.product_type.aanmaak_datum.astimezone().isoformat()
)
product_dict["product_type"]["update_datum"] = str(
product.product_type.update_datum.astimezone().isoformat()
)
return product_dict


@freeze_time("2024-01-01")
Expand All @@ -53,27 +26,68 @@ def setUp(self):
"eind_datum": datetime.date(2024, 12, 31),
"data": [],
}
self.path = "/api/v1/producten/"
self.path = reverse("product-list")

def detail_path(self, product_type):
return reverse("product-detail", args=[product_type.id])

def test_read_product_without_credentials_returns_error(self):
response = APIClient().get(self.path)
self.assertEqual(response.status_code, 401)

def _create_product(self):
return ProductFactory.create(bsn="111222333")
def test_required_fields(self):
response = self.client.post(self.path, {})

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data,
{
"product_type_id": [
ErrorDetail(string="Dit veld is vereist.", code="required")
],
"start_datum": [
ErrorDetail(string="Dit veld is vereist.", code="required")
],
"eind_datum": [
ErrorDetail(string="Dit veld is vereist.", code="required")
],
},
)

def test_create_product(self):
response = self.post(self.data)
response = self.client.post(self.path, self.data)

self.assertEqual(response.status_code, 201)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Product.objects.count(), 1)
product = Product.objects.first()
self.assertEqual(response.data, product_to_dict(product))
product_type = product.product_type
expected_data = {
"id": str(product.id),
"bsn": product.bsn,
"kvk": product.kvk,
"gepubliceerd": False,
"start_datum": str(product.start_datum),
"eind_datum": str(product.eind_datum),
"aanmaak_datum": product.aanmaak_datum.astimezone().isoformat(),
"update_datum": product.update_datum.astimezone().isoformat(),
"product_type": {
"id": str(product_type.id),
"naam": product_type.naam,
"samenvatting": product_type.samenvatting,
"beschrijving": product_type.beschrijving,
"uniforme_product_naam": product_type.uniforme_product_naam.uri,
"gepubliceerd": True,
"aanmaak_datum": product_type.aanmaak_datum.astimezone().isoformat(),
"update_datum": product_type.update_datum.astimezone().isoformat(),
"keywords": [],
},
}
self.assertEqual(response.data, expected_data)

def test_create_product_without_bsn_or_kvk_returns_error(self):
data = self.data.copy()
data.pop("bsn")
response = self.post(data)
response = self.client.post(self.path, data)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data,
Expand All @@ -89,21 +103,20 @@ def test_create_product_without_bsn_or_kvk_returns_error(self):
self.assertEqual(Product.objects.count(), 0)

def test_update_product(self):
product = self._create_product()
product = ProductFactory.create(bsn="111222333")

data = self.data | {"eind_datum": datetime.date(2025, 12, 31)}
response = self.put(product.id, data)
response = self.client.put(self.detail_path(product), data)

self.assertEqual(response.status_code, 200)
self.assertEqual(Product.objects.count(), 1)
self.assertEqual(Product.objects.first().eind_datum, data["eind_datum"])

def test_update_product_without_bsn_or_kvk(self):
product = self._create_product()
product = ProductFactory.create(bsn="111222333")

data = self.data.copy()
data.pop("bsn")
response = self.put(product.id, data)
data = self.data | {"bsn": None}
response = self.client.put(self.detail_path(product), data)

self.assertEqual(response.status_code, 400)
self.assertEqual(
Expand All @@ -119,34 +132,105 @@ def test_update_product_without_bsn_or_kvk(self):
)

def test_partial_update_product(self):
product = self._create_product()
product = ProductFactory.create(bsn="111222333")

data = {"eind_datum": datetime.date(2025, 12, 31)}
response = self.patch(product.id, data)
response = self.client.patch(self.detail_path(product), data)

self.assertEqual(response.status_code, 200)
self.assertEqual(Product.objects.count(), 1)
self.assertEqual(Product.objects.first().eind_datum, data["eind_datum"])

def test_read_producten(self):
product = self._create_product()
product1 = ProductFactory.create(
bsn="111222333", product_type=self.product_type
)
product2 = ProductFactory.create(
bsn="111222444", product_type=self.product_type
)

response = self.get()
response = self.client.get(self.path)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 1)
self.assertEqual(response.data["results"], [product_to_dict(product)])
self.assertEqual(response.data["count"], 2)
expected_data = [
{
"id": str(product1.id),
"bsn": product1.bsn,
"kvk": product1.kvk,
"gepubliceerd": False,
"start_datum": str(product1.start_datum),
"eind_datum": str(product1.eind_datum),
"aanmaak_datum": product1.aanmaak_datum.astimezone().isoformat(),
"update_datum": product1.update_datum.astimezone().isoformat(),
"product_type": {
"id": str(self.product_type.id),
"naam": self.product_type.naam,
"samenvatting": self.product_type.samenvatting,
"beschrijving": self.product_type.beschrijving,
"uniforme_product_naam": self.product_type.uniforme_product_naam.uri,
"gepubliceerd": True,
"aanmaak_datum": self.product_type.aanmaak_datum.astimezone().isoformat(),
"update_datum": self.product_type.update_datum.astimezone().isoformat(),
"keywords": [],
},
},
{
"id": str(product2.id),
"bsn": product2.bsn,
"kvk": product2.kvk,
"gepubliceerd": False,
"start_datum": str(product2.start_datum),
"eind_datum": str(product2.eind_datum),
"aanmaak_datum": product2.aanmaak_datum.astimezone().isoformat(),
"update_datum": product2.update_datum.astimezone().isoformat(),
"product_type": {
"id": str(self.product_type.id),
"naam": self.product_type.naam,
"samenvatting": self.product_type.samenvatting,
"beschrijving": self.product_type.beschrijving,
"uniforme_product_naam": self.product_type.uniforme_product_naam.uri,
"gepubliceerd": True,
"aanmaak_datum": self.product_type.aanmaak_datum.astimezone().isoformat(),
"update_datum": self.product_type.update_datum.astimezone().isoformat(),
"keywords": [],
},
},
]
self.assertCountEqual(response.data["results"], expected_data)

def test_read_product(self):
product = self._create_product()
product = ProductFactory.create(bsn="111222333", product_type=self.product_type)

response = self.get(product.id)
response = self.client.get(self.detail_path(product))

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, product_to_dict(product))
expected_data = {
"id": str(product.id),
"bsn": product.bsn,
"kvk": product.kvk,
"gepubliceerd": False,
"start_datum": str(product.start_datum),
"eind_datum": str(product.eind_datum),
"aanmaak_datum": product.aanmaak_datum.astimezone().isoformat(),
"update_datum": product.update_datum.astimezone().isoformat(),
"product_type": {
"id": str(self.product_type.id),
"naam": self.product_type.naam,
"samenvatting": self.product_type.samenvatting,
"beschrijving": self.product_type.beschrijving,
"uniforme_product_naam": self.product_type.uniforme_product_naam.uri,
"gepubliceerd": True,
"aanmaak_datum": self.product_type.aanmaak_datum.astimezone().isoformat(),
"update_datum": self.product_type.update_datum.astimezone().isoformat(),
"keywords": [],
},
}
self.assertEqual(response.data, expected_data)

def test_delete_product(self):
product = self._create_product()
response = self.delete(product.id)
product = ProductFactory.create(bsn="111222333")
response = self.client.delete(self.detail_path(product))

self.assertEqual(response.status_code, 204)
self.assertEqual(Product.objects.count(), 0)
11 changes: 2 additions & 9 deletions src/open_producten/producten/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
from django_filters.rest_framework import DjangoFilterBackend

from open_producten.producten.models import Product
from open_producten.producten.serializers.product import (
ProductSerializer,
ProductUpdateSerializer,
)
from open_producten.producten.serializers.product import ProductSerializer
from open_producten.utils.views import OrderedModelViewSet


class ProductViewSet(OrderedModelViewSet):
queryset = Product.objects.all()
lookup_url_field = "id"
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ["gepubliceerd"]

def get_serializer_class(self):
if self.action in ("update", "partial_update"):
return ProductUpdateSerializer
return ProductSerializer
Loading

0 comments on commit a29bdbd

Please sign in to comment.