-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #140 from maykinmedia/task-337-reinstantiate-pdc-api
[#337] Feature/API for categories and products
- Loading branch information
Showing
7 changed files
with
386 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
from typing import List | ||
|
||
from drf_spectacular.utils import extend_schema_field | ||
from filer.models import File, Image | ||
from rest_framework import serializers | ||
|
||
from open_inwoner.pdc.models import Category, Product, ProductLink, Question, Tag | ||
from open_inwoner.pdc.models.organization import Organization | ||
from open_inwoner.pdc.models.product import ( | ||
ProductCondition, | ||
ProductContact, | ||
ProductFile, | ||
ProductLocation, | ||
) | ||
|
||
|
||
class FilerImageSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = Image | ||
fields = ( | ||
"name", | ||
"description", | ||
"file", | ||
"subject_location", | ||
) | ||
|
||
|
||
class FilerFileSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = File | ||
fields = ( | ||
"name", | ||
"description", | ||
"file", | ||
) | ||
|
||
|
||
class ProductFileSerializer(serializers.ModelSerializer): | ||
file = FilerFileSerializer(required=False) | ||
|
||
class Meta: | ||
model = ProductFile | ||
fields = ("file",) | ||
|
||
|
||
class TagSerializer(serializers.ModelSerializer): | ||
icon = FilerImageSerializer(required=False) | ||
type = serializers.StringRelatedField() | ||
|
||
class Meta: | ||
model = Tag | ||
fields = ("name", "slug", "icon", "type") | ||
|
||
|
||
class SmallProductSerializer(serializers.HyperlinkedModelSerializer): | ||
class Meta: | ||
model = Product | ||
fields = ("url", "name", "slug", "summary") | ||
extra_kwargs = { | ||
"url": {"view_name": "api:products-detail", "lookup_field": "slug"}, | ||
} | ||
|
||
|
||
class Questionserializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = Question | ||
fields = ("question", "answer") | ||
|
||
|
||
class SmallCategorySerializer(serializers.HyperlinkedModelSerializer): | ||
class Meta: | ||
model = Category | ||
fields = ("url", "name", "slug", "description") | ||
extra_kwargs = { | ||
"url": {"view_name": "api:categories-detail", "lookup_field": "slug"}, | ||
} | ||
|
||
|
||
class CategoryWithChildSerializer(serializers.ModelSerializer): | ||
icon = FilerImageSerializer(required=False) | ||
image = FilerImageSerializer(required=False) | ||
products = SmallProductSerializer(required=False, many=True) | ||
questions = Questionserializer(required=False, many=True, source="question_set") | ||
children = serializers.SerializerMethodField() | ||
|
||
class Meta: | ||
model = Category | ||
fields = ( | ||
"name", | ||
"slug", | ||
"description", | ||
"icon", | ||
"image", | ||
"products", | ||
"questions", | ||
"children", | ||
) | ||
|
||
@extend_schema_field(SmallCategorySerializer(many=True)) | ||
def get_children(self, obj): | ||
return SmallCategorySerializer( | ||
obj.get_children(), many=True, context=self._context | ||
).data | ||
|
||
|
||
class OrganizationSerializer(serializers.ModelSerializer): | ||
logo = FilerImageSerializer(required=False) | ||
type = serializers.StringRelatedField() | ||
neighbourhood = serializers.StringRelatedField() | ||
|
||
class Meta: | ||
model = Organization | ||
fields = ( | ||
"name", | ||
"slug", | ||
"logo", | ||
"type", | ||
"email", | ||
"phonenumber", | ||
"neighbourhood", | ||
) | ||
|
||
|
||
class ProductContactSerializer(serializers.ModelSerializer): | ||
organization = serializers.StringRelatedField() | ||
|
||
class Meta: | ||
model = ProductContact | ||
fields = ( | ||
"organization", | ||
"first_name", | ||
"last_name", | ||
"email", | ||
"phonenumber", | ||
"role", | ||
) | ||
|
||
|
||
class ProductLocationSerializer(serializers.ModelSerializer): | ||
coordinates = serializers.SerializerMethodField() | ||
|
||
class Meta: | ||
model = ProductLocation | ||
fields = ("name", "street", "housenumber", "postcode", "city", "coordinates") | ||
|
||
@extend_schema_field(List[str]) | ||
def get_coordinates(self, obj): | ||
return obj.geometry.coords | ||
|
||
|
||
class ProductConditionSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = ProductCondition | ||
fields = ("name", "question", "positive_text", "negative_text", "rule") | ||
|
||
|
||
class ProductLinkSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = ProductLink | ||
fields = ( | ||
"name", | ||
"url", | ||
) | ||
|
||
|
||
class ProductSerializer(serializers.ModelSerializer): | ||
links = ProductLinkSerializer(many=True, required=False) | ||
categories = SmallCategorySerializer(many=True, required=False) | ||
related_products = SmallProductSerializer(many=True, required=False) | ||
tags = TagSerializer(many=True, required=False) | ||
organizations = OrganizationSerializer(many=True, required=False) | ||
contacts = ProductContactSerializer(many=True, required=False) | ||
locations = ProductLocationSerializer(many=True, required=False) | ||
conditions = ProductConditionSerializer(many=True, required=False) | ||
files = ProductFileSerializer(many=True, required=False) | ||
|
||
class Meta: | ||
model = Product | ||
fields = ( | ||
"name", | ||
"slug", | ||
"summary", | ||
"link", | ||
"content", | ||
"categories", | ||
"related_products", | ||
"tags", | ||
"costs", | ||
"created_on", | ||
"organizations", | ||
"links", | ||
"keywords", | ||
"uniforme_productnaam", | ||
"contacts", | ||
"locations", | ||
"conditions", | ||
"files", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from rest_framework import viewsets | ||
from rest_framework.generics import get_object_or_404 | ||
|
||
from open_inwoner.pdc.models import Category, Product | ||
|
||
from .serializers import CategoryWithChildSerializer, ProductSerializer | ||
|
||
|
||
class CategoryViewSet(viewsets.ReadOnlyModelViewSet): | ||
authentication_classes = [] | ||
permission_classes = [] | ||
serializer_class = CategoryWithChildSerializer | ||
lookup_field = "slug" | ||
|
||
def get_object(self): | ||
""" | ||
Returns the object the view is displaying. | ||
You may want to override this if you need to provide non-standard | ||
queryset lookups. Eg if objects are referenced using multiple | ||
keyword arguments in the url conf. | ||
""" | ||
queryset = self.filter_queryset(Category.objects.all()) | ||
|
||
# Perform the lookup filtering. | ||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field | ||
|
||
assert lookup_url_kwarg in self.kwargs, ( | ||
"Expected view %s to be called with a URL keyword argument " | ||
'named "%s". Fix your URL conf, or set the `.lookup_field` ' | ||
"attribute on the view correctly." | ||
% (self.__class__.__name__, lookup_url_kwarg) | ||
) | ||
|
||
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} | ||
obj = get_object_or_404(queryset, **filter_kwargs) | ||
|
||
# May raise a permission denied | ||
self.check_object_permissions(self.request, obj) | ||
|
||
return obj | ||
|
||
def get_queryset(self): | ||
return Category.get_root_nodes() | ||
|
||
|
||
class ProductViewSet(viewsets.ReadOnlyModelViewSet): | ||
authentication_classes = [] | ||
permission_classes = [] | ||
serializer_class = ProductSerializer | ||
queryset = Product.objects.prefetch_related( | ||
"links", | ||
"categories", | ||
"related_products", | ||
"tags", | ||
"organizations", | ||
"contacts", | ||
"locations", | ||
"conditions", | ||
) | ||
lookup_field = "slug" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.urls import reverse | ||
|
||
from rest_framework import status | ||
from rest_framework.test import APIClient, APITestCase | ||
|
||
from open_inwoner.pdc.tests.factories import ProductFactory, ProductLocationFactory | ||
|
||
|
||
class TestPDCLocation(APITestCase): | ||
def setUp(self): | ||
self.client = APIClient() | ||
|
||
def test_products_endpoint_returns_location_coordinates(self): | ||
location = ProductLocationFactory() | ||
ProductFactory(locations=(location,)) | ||
|
||
response = self.client.get(reverse("api:products-list"), format="json") | ||
|
||
coordinates = response.json()[0]["locations"][0]["coordinates"] | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(coordinates, [5.0, 52.0]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
from unicodedata import category | ||
|
||
from django.urls import reverse | ||
|
||
from rest_framework import status | ||
from rest_framework.test import APIClient, APITestCase | ||
|
||
from open_inwoner.pdc.models import Category | ||
from open_inwoner.pdc.tests.factories import CategoryFactory | ||
|
||
|
||
class TestPDCLocation(APITestCase): | ||
def setUp(self): | ||
self.client = APIClient() | ||
|
||
self.root_category_1 = CategoryFactory.build() | ||
self.child_category_1 = CategoryFactory.build() | ||
self.grandchild_category = CategoryFactory.build() | ||
Category.add_root(instance=self.root_category_1) | ||
self.root_category_1.add_child(instance=self.child_category_1) | ||
self.child_category_1.add_child(instance=self.grandchild_category) | ||
|
||
self.root_category_2 = CategoryFactory.build() | ||
self.child_category_2 = CategoryFactory.build() | ||
Category.add_root(instance=self.root_category_2) | ||
self.root_category_2.add_child(instance=self.child_category_2) | ||
|
||
def test_list_categories_endpoint_returns_both_parent_and_children_categories(self): | ||
response = self.client.get(reverse("api:categories-list"), format="json") | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEquals( | ||
sorted(response.json(), key=lambda k: k["slug"]), | ||
sorted( | ||
[ | ||
{ | ||
"name": self.root_category_1.name, | ||
"slug": self.root_category_1.slug, | ||
"description": self.root_category_1.description, | ||
"icon": None, | ||
"image": None, | ||
"products": [], | ||
"questions": [], | ||
"children": [ | ||
{ | ||
"url": f"http://testserver/api/categories/{self.child_category_1.slug}/", | ||
"name": self.child_category_1.name, | ||
"slug": self.child_category_1.slug, | ||
"description": self.child_category_1.description, | ||
} | ||
], | ||
}, | ||
{ | ||
"name": self.root_category_2.name, | ||
"slug": self.root_category_2.slug, | ||
"description": self.root_category_2.description, | ||
"icon": None, | ||
"image": None, | ||
"products": [], | ||
"questions": [], | ||
"children": [ | ||
{ | ||
"url": f"http://testserver/api/categories/{self.child_category_2.slug}/", | ||
"name": self.child_category_2.name, | ||
"slug": self.child_category_2.slug, | ||
"description": self.child_category_2.description, | ||
} | ||
], | ||
}, | ||
], | ||
key=lambda k: k["slug"], | ||
), | ||
) | ||
|
||
def test_category_detail_endpoint_returns_both_parent_and_children_categories(self): | ||
response = self.client.get( | ||
reverse( | ||
"api:categories-detail", kwargs={"slug": self.child_category_1.slug} | ||
), | ||
format="json", | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEquals( | ||
response.json(), | ||
{ | ||
"name": self.child_category_1.name, | ||
"slug": self.child_category_1.slug, | ||
"description": self.child_category_1.description, | ||
"icon": None, | ||
"image": None, | ||
"products": [], | ||
"questions": [], | ||
"children": [ | ||
{ | ||
"url": f"http://testserver/api/categories/{self.grandchild_category.slug}/", | ||
"name": self.grandchild_category.name, | ||
"slug": self.grandchild_category.slug, | ||
"description": self.grandchild_category.description, | ||
} | ||
], | ||
}, | ||
) |
Oops, something went wrong.