Skip to content

Commit

Permalink
Merge pull request #117 from django-oscar/feature/parent-child
Browse files Browse the repository at this point in the history
Feature/parent child
  • Loading branch information
Martijn Jacobs committed Jan 16, 2018
2 parents fcad9ab + 0dbec3f commit 857b8f9
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 56 deletions.
6 changes: 3 additions & 3 deletions oscarapi/fixtures/product.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
<django-objects version="1.0">
<object pk="1" model="catalogue.product">
<field type="CharField" name="structure">parent</field>
<field type="CharField" name="upc"/>
<field type="CharField" name="upc">1234</field>
<field to="catalogue.product" name="parent" rel="ManyToOneRel">
<None/>
</field>
<field type="CharField" name="title">Oscar T-shirt</field>
<field type="SlugField" name="slug">oscar-t-shirt</field>
<field type="TextField" name="description"/>
<field type="TextField" name="description">Hank</field>
<field to="catalogue.productclass" name="product_class" rel="ManyToOneRel">1</field>
<field type="FloatField" name="rating">
<None/>
Expand All @@ -20,7 +20,7 @@
</object>
<object pk="2" model="catalogue.product">
<field type="CharField" name="structure">child</field>
<field type="CharField" name="upc"/>
<field type="CharField" name="upc">child-1234</field>
<field to="catalogue.product" name="parent" rel="ManyToOneRel">1</field>
<field type="CharField" name="title"/>
<field type="SlugField" name="slug">oscar-t-shirt</field>
Expand Down
59 changes: 46 additions & 13 deletions oscarapi/serializers/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,6 @@ class Meta:
))


class ProductLinkSerializer(OscarHyperlinkedModelSerializer):
class Meta:
model = Product
fields = overridable(
'OSCARAPI_PRODUCT_FIELDS', default=(
'url', 'id', 'title'
))


class ProductAttributeValueSerializer(OscarModelSerializer):
name = serializers.StringRelatedField(source="attribute")
value = serializers.SerializerMethodField()
Expand Down Expand Up @@ -108,31 +99,73 @@ class Meta:
'OSCARAPI_RECOMMENDED_PRODUCT_FIELDS', default=('url',))


class ProductSerializer(OscarModelSerializer):
class BaseProductSerializer(OscarModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='product-detail')
stockrecords = serializers.HyperlinkedIdentityField(
view_name='product-stockrecord-list')
attributes = ProductAttributeValueSerializer(
many=True, required=False, source="attribute_values")
categories = serializers.StringRelatedField(many=True, required=False)
product_class = serializers.StringRelatedField(required=False)
images = ProductImageSerializer(many=True, required=False)
price = serializers.HyperlinkedIdentityField(view_name='product-price')
availability = serializers.HyperlinkedIdentityField(
view_name='product-availability')
options = OptionSerializer(many=True, required=False)
recommended_products = RecommmendedProductSerializer(
many=True, required=False)

def get_field_names(self, declared_fields, info):
"""
Override get_field_names to make sure that we are not getting errors
for not including declared fields.
"""
return super(BaseProductSerializer, self).get_field_names({}, info)

class Meta:
model = Product


class ChildProductserializer(BaseProductSerializer):
parent = serializers.HyperlinkedRelatedField(
view_name='product-detail', queryset=Product.objects)
# the below fields can be filled from the parent product if enabled.
images = ProductImageSerializer(many=True, required=False, source='parent.images')
description = serializers.CharField(source='parent.description')

class Meta(BaseProductSerializer.Meta):
fields = overridable(
'OSCARAPI_CHILDPRODUCTDETAIL_FIELDS',
default=(
'url', 'id', 'title', 'structure',
# 'parent', 'description', 'images', are not included by default, but
# easily enabled by overriding OSCARAPI_CHILDPRODUCTDETAIL_FIELDS
# in your settings file
'date_created', 'date_updated', 'recommended_products',
'attributes', 'categories', 'product_class',
'stockrecords', 'price', 'availability', 'options'))


class ProductSerializer(BaseProductSerializer):
images = ProductImageSerializer(many=True, required=False)
children = ChildProductserializer(many=True, required=False)

class Meta(BaseProductSerializer.Meta):
fields = overridable(
'OSCARAPI_PRODUCTDETAIL_FIELDS',
default=(
'url', 'id', 'title', 'description',
'url', 'id', 'title', 'description', 'structure',
'date_created', 'date_updated', 'recommended_products',
'attributes', 'categories', 'product_class',
'stockrecords', 'images', 'price', 'availability', 'options'))
'stockrecords', 'images', 'price', 'availability', 'options',
'children'))


class ProductLinkSerializer(ProductSerializer):
class Meta(BaseProductSerializer.Meta):
fields = overridable(
'OSCARAPI_PRODUCT_FIELDS', default=(
'url', 'id', 'title'
))


class OptionValueSerializer(serializers.Serializer):
Expand Down
93 changes: 89 additions & 4 deletions oscarapi/tests/testproduct.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import mock
from six import string_types

from oscarapi.tests.utils import APITest
from django.core.urlresolvers import reverse

from oscarapi.tests.utils import APITest
from oscarapi.serializers.product import ProductLinkSerializer


class ProductListDetailSerializer(ProductLinkSerializer):
"subclass of ProductLinkSerializer to demonstrate showing details in listview"
class Meta(ProductLinkSerializer.Meta):
fields = (
'url', 'id', 'title', 'structure', 'description', 'date_created',
'date_updated', 'recommended_products', 'attributes',
'categories', 'product_class', 'stockrecords', 'images',
'price', 'availability', 'options', 'children'
)


class ProductTest(APITest):
fixtures = [
Expand All @@ -23,15 +37,86 @@ def test_product_list(self):
for field in default_fields:
self.assertIn(field, product)

def test_product_list_filter(self):
standalone_products_url = "%s?structure=standalone" % reverse('product-list')
self.response = self.get(standalone_products_url)
self.response.assertStatusEqual(200)
self.assertEqual(len(self.response.body), 2)

parent_products_url = "%s?structure=parent" % reverse('product-list')
self.response = self.get(parent_products_url)
self.response.assertStatusEqual(200)
self.assertEqual(len(self.response.body), 1)

child_products_url = "%s?structure=child" % reverse('product-list')
self.response = self.get(child_products_url)
self.response.assertStatusEqual(200)
self.assertEqual(len(self.response.body), 1)

koe_products_url = "%s?structure=koe" % reverse('product-list')
self.response = self.get(koe_products_url)
self.response.assertStatusEqual(200)
self.assertEqual(len(self.response.body), 0)

@mock.patch('oscarapi.views.product.ProductList.get_serializer_class')
def test_productlist_detail(self, get_serializer_class):
"The product list should be able to render the same information as the detail page"
# setup mocks
get_serializer_class.return_value = ProductListDetailSerializer

# define fields to check
product_detail_fields = (
'url', 'id', 'title', 'structure', 'description', 'date_created',
'date_updated', 'recommended_products', 'attributes',
'categories', 'product_class', 'stockrecords', 'images',
'price', 'availability', 'options', 'children'
)

# fetch data
parent_products_url = "%s?structure=parent" % reverse('product-list')
self.response = self.get(parent_products_url)

# make assertions
get_serializer_class.assert_called_once_with()
self.response.assertStatusEqual(200)
self.assertEqual(len(self.response.body), 1)

# load product data
products = self.response.json()
product_with_children = products[0]

self.assertEqual(product_with_children['structure'], 'parent',
"since we filtered on structure=parent the list should contain items with structure=parent"
)
# verify all the fields are rendered in the list view
for field in product_detail_fields:
self.assertIn(field, product_with_children)

children = product_with_children['children']
self.assertEqual(len(children), 1, "There should be 1 child")
child = children[0]

self.assertEqual(child['structure'], 'child',
"the child should have structure=child"
)
self.assertNotEqual(product_with_children['id'], child['id'])
self.assertNotEqual(product_with_children['structure'], child['structure'])
self.assertNotIn('description', child, "child should not have a description by default")
self.assertNotIn('images', child, "child should not have images by default")

def test_product_detail(self):
"Check product details"
self.response = self.get(reverse('product-detail', args=(1,)))
self.response.assertStatusEqual(200)
default_fields = ['stockrecords', 'description', 'title', 'url',
'date_updated', 'recommended_products', 'attributes',
'date_created', 'id', 'price', 'availability']
default_fields = (
'url', 'id', 'title', 'structure', 'description', 'date_created',
'date_updated', 'recommended_products', 'attributes',
'categories', 'product_class', 'stockrecords', 'images',
'price', 'availability', 'options', 'children'
)
for field in default_fields:
self.assertIn(field, self.response.body)

self.response.assertValueEqual('title', "Oscar T-shirt")

def test_product_attribute_entity(self):
Expand Down
1 change: 1 addition & 0 deletions oscarapi/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from oscarapi.views.login import *
from oscarapi.views.basket import *
from oscarapi.views.checkout import *
from oscarapi.views.product import *
36 changes: 0 additions & 36 deletions oscarapi/views/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
__all__ = (
'BasketList', 'BasketDetail',
'LineAttributeList', 'LineAttributeDetail',
'ProductList', 'ProductDetail',
'ProductPrice', 'ProductAvailability',
'StockRecordList', 'StockRecordDetail',
'UserList', 'UserDetail',
'OptionList', 'OptionDetail',
Expand Down Expand Up @@ -81,40 +79,6 @@ class LineAttributeDetail(generics.RetrieveAPIView):
serializer_class = serializers.LineAttributeSerializer


class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = serializers.ProductLinkSerializer


class ProductDetail(generics.RetrieveAPIView):
queryset = Product.objects.all()
serializer_class = serializers.ProductSerializer


class ProductPrice(generics.RetrieveAPIView):
queryset = Product.objects.all()

def get(self, request, pk=None, format=None):
product = self.get_object()
strategy = Selector().strategy(request=request, user=request.user)
ser = serializers.PriceSerializer(
strategy.fetch_for_product(product).price,
context={'request': request})
return Response(ser.data)


class ProductAvailability(generics.RetrieveAPIView):
queryset = Product.objects.all()

def get(self, request, pk=None, format=None):
product = self.get_object()
strategy = Selector().strategy(request=request, user=request.user)
ser = serializers.AvailabilitySerializer(
strategy.fetch_for_product(product).availability,
context={'request': request})
return Response(ser.data)


class StockRecordList(generics.ListAPIView):
serializer_class = serializers.StockRecordSerializer
queryset = StockRecord.objects.all()
Expand Down
69 changes: 69 additions & 0 deletions oscarapi/views/product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from oscar.core.loading import get_model, get_class
from rest_framework import generics
from rest_framework.response import Response

from oscarapi import serializers


Selector = get_class('partner.strategy', 'Selector')

__all__ = (
'ProductList', 'ProductDetail',
'ProductPrice', 'ProductAvailability',
)

Product = get_model('catalogue', 'Product')


class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = serializers.ProductLinkSerializer

def get_queryset(self):
"""
Allow filtering on structure so standalone and parent products can
be selected separately, eg::
http://127.0.0.1:8000/api/products/?structure=standalone
or::
http://127.0.0.1:8000/api/products/?structure=parent
"""
qs = super(ProductList, self).get_queryset()
structure = self.request.query_params.get('structure')
if structure is not None:
return qs.filter(structure=structure)

return qs


class ProductDetail(generics.RetrieveAPIView):
queryset = Product.objects.all()
serializer_class = serializers.ProductSerializer


class ProductPrice(generics.RetrieveAPIView):
queryset = Product.objects.all()

def get(self, request, pk=None, format=None):
product = self.get_object()
strategy = Selector().strategy(request=request, user=request.user)
ser = serializers.PriceSerializer(
strategy.fetch_for_product(product).price,
context={'request': request})
return Response(ser.data)


class ProductAvailability(generics.RetrieveAPIView):
queryset = Product.objects.all()

def get(self, request, pk=None, format=None):
product = self.get_object()
strategy = Selector().strategy(request=request, user=request.user)
ser = serializers.AvailabilitySerializer(
strategy.fetch_for_product(product).availability,
context={'request': request})
return Response(ser.data)


0 comments on commit 857b8f9

Please sign in to comment.