Test Django REST framework API response with nested resources - django

I'm writing an REST APIs for items and the menu endpoint is returning a JSON with items groups inside it the items inside the items any extra the user can add to the item.
serializers.py
====================
class ItemExtraSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ItemExtra
fields = ('id', 'name', 'price')
class ItemSerializer(serializers.HyperlinkedModelSerializer):
extras = ItemExtraSerializer(many=True, read_only=True)
class Meta:
model = Item
fields = ('id', 'url', 'name', 'description', 'image', 'code', 'price', 'extras')
class ItemGroupSerializer(serializers.HyperlinkedModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = ItemGroup
fields = ('id', 'url', 'name', 'items')
views.py
=========================
class MenuView(ListAPIView):
serializer_class = ItemGroupSerializer
def get_queryset(self):
"""
Return the items inside their groups for search query
Filtering is one Group name and Item name
:return:
"""
queryset = ItemGroup.objects.all()
search_terms = self.request.query_params.get('q', None)
if search_terms:
queryset = Item.objects.search(search_terms)
return queryset
tests.py
========================
class ItemTestCases(APITestCase):
def setUp(self):
self.sandwich_group, created = ItemGroup.objects.get_or_create(name='Sandwiches')
self.meal_group, created = ItemGroup.objects.get_or_create(name='Meals')
self.shawarma_sandwich, created = Item.objects.get_or_create(name='Shawarma Sandwich',
description='Meat Shawarma Sandwich',
price=1.250,
code='SW-01',
group=self.sandwich_group)
self.burger_sandwich, created = Item.objects.get_or_create(name='Cheese Burger',
description='Single cheese burger Sandwich',
price=1.000,
code='SW-02',
group=self.sandwich_group)
self.burger_sandwich_extra, created = ItemExtra.objects.get_or_create(name='Extra cheese',
price=0.100,
item=self.burger_sandwich)
self.sharawma_meal, created = Item.objects.get_or_create(name='Shawarma Meal',
description='Shawarma Sandwich with fries and drink',
price=2.000,
code='ME-01',
group=self.meal_group)
self.burger_meal, created = Item.objects.get_or_create(name='Burger Meal',
description='Single cheese burger Sandwich',
price=2.250,
code='ME-02',
group=self.meal_group)
self.cheese_meal_extra, created = ItemExtra.objects.get_or_create(name='Extra cheese',
price=0.100,
item=self.burger_meal)
self.factory = APIRequestFactory()
def test_menu_GET_request(self):
item_list = reverse('menu')
response = self.client.get(item_list)
self.assertEqual(response.status_code, status.HTTP_200_OK)
groups = ItemGroup.objects.all()
expected = ItemGroupSerializer(groups)
self.assertContains(response, expected.data)
The output of the test is:
AssertionError: HyperlinkedIdentityField requires the request in the serializer context. Add context={'request': request} when instantiating the serializer.
How to render the serlizer to JSON in order to compare it with the API endpoint JSON?
Update 1:
I figure it out but I think there is a cleaner solution
I've created helper function
def render_many_serializer_as_json(serializer, request, instance):
serializer_data = serializer(instance=instance, many=True, context={'request': request}).data
return JSONRenderer().render(serializer_data)
And rewrite my test
def test_menu_GET_request(self):
item_list = reverse('menu')
request = self.factory.get(item_list, format='json')
response = self.client.get(item_list)
groups = ItemGroup.objects.all()
expected = render_many_serializer_as_json(ItemGroupSerializer, request, groups)
self.assertEqual(response.content, expected)

Why do something complex ?
Just explicitly write down the expected JSON output:
expected = {
'id': 1,
'name': ...,
...,
'items': [{
'id': ..,
'url': ...,
}]
}

Related

Django Rest UpdateView: This field is required

I have the following view:
class CampaignUpdate(generics.RetrieveUpdateAPIView):
queryset = Campaign.objects.all()
serializer_class = CampaignSerializer
permission_classes = [CampaignDetailPermission]
lookup_field = 'cid'
And I have the following test function:
def test_update_campaign(self):
request = self.factory.put(f'/', {'name': 'Updated Campaign'})
force_authenticate(request, user=self.merchant)
response = CampaignUpdate.as_view()(request, cid=self.campaign.cid)
# Check that the response status code is 200 (OK)
print(response.data)
self.assertEqual(response.status_code, 200)
# Check that the campaign was updated in the database
self.assertEqual(Campaign.objects.get(cid=self.campaign.cid).name, 'Updated Campaign')
And I am getting the following error:
System check identified 4 issues (0 silenced).
..........{'shop': [ErrorDetail(string='This field is required.', code='required')]}
shop field is required for creation but i do not want to update this field everytime I update some other field.
How can I make it optional for update or readonly after creation?
In case, you need to see serializer:
class CampaignSerializer(serializers.ModelSerializer):
class Meta:
model = Campaign
fields = '__all__'
I have updated the serizlier:
class CampaignSerializer(serializers.ModelSerializer):
class Meta:
model = Campaign
# fields = ['cid', 'name', 'shop', 'end_date', 'start_date', 'active']
# read_only_fields = ['cid']
fields = '__all__'
def update(self, instance: Campaign, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.end_date = validated_data.get('end_date', instance.end_date)
instance.start_date = validated_data.get('start_date', instance.start_date)
instance.active = validated_data.get('active', instance.active)
instance.shop = instance.shop
print(instance)
instance.save()
return instance

Django REST framework, serializer performance degradation

I have a simple list API view which is using serializer:
class ListCreateDeploymentView(
generics.ListCreateAPIView
):
permission_classes = (IsAuthenticated,)
renderer_classes = [JSONRenderer]
content_negotiation_class = IgnoreClientContentNegotiation
def get_queryset(self):
queryset = Deployment.objects.all()
return queryset
def list(self, request, version):
queryset = self.get_queryset()
serializer = DeploymentListSerializer(queryset, many=True)
data = serializer.data
return Response(data)
Serializer is simple:
class DeploymentListSerializer(serializers.ModelSerializer):
class Meta:
model = Deployment
fields = (
'id',
'query',
'config',
'started_at',
'finished_at',
'status',
'project',
)
read_only_fields = (
'id',
'query',
'config',
'started_at',
'finished_at',
'status',
'project',
)
Then I do a local load test with 10 users and delay 1s each execution so target rps is 10 req/s and see this picture with a clear performance degradation after few minutes
What means If I open 10 tabs in browser with ajax request every second to this endpoint the server will get unresponsive in a minute:
Then I used recommendations from here and used read-only regular serializer:
class DeploymentListSerializer(serializers.ModelSerializer):
# commands = CommandListSerializer(read_only=True, many=True)
# clients = ClientSimpleSerializer(read_only=True, many=True)
id = serializers.IntegerField(read_only=True)
query = serializers.CharField(read_only=True)
config = serializers.CharField(read_only=True)
started_at = serializers.DateTimeField(read_only=True)
finished_at = serializers.DateTimeField(read_only=True)
status = serializers.IntegerField(read_only=True)
project = serializers.CharField(read_only=True)
class Meta:
model = Deployment
fields = (
'id',
'query',
'config',
'started_at',
'finished_at',
'status',
'project',
)
The situation became even worse:
Finally, if I remove serialization:
def list(self, request, version):
queryset = self.get_queryset()
data = queryset.values(
'id', 'query', 'config', 'started_at', 'finished_at',
'status', 'project'
)
return Response(data)
And do same test again the performance getting much better(expected), but also stable:
The problem is I need serialization because the task is a bit more complicated and I need to return nested objects, but it`s already falling on such an easy example.
What do I wrong?
UPD: same bad picture if I use the function-based view:
#api_view(['GET'])
def get_deployments(request, version):
queryset = Deployment.objects.all()
serializer = DeploymentCreateSerializer(queryset, many=True)
data = serializer.data
return Response(data)

Django Rest Framework SerializerMethodField only on GET Request

Running into a little snag here with my DRF backend.
I am populating fields with choices on certain models.
I have a foreign key requirement on one model. When I create the model I want to save it under the foreign id.
When I request the models, I want the model with whatever the choice field maps to.
I was able to do this with SerializerMethodField, however when I try to create a model, I get a 400 error because the block is not valid. If I remove the SerializerMethodField, I can save, but get the number stored in the db from the request.
Any help would be appreciated.
class BlockViewSet(ModelViewSet):
model = apps.get_model('backend', 'Block')
queryset = model.objects.all()
serializer_class = serializers.BlockSerializer
permissions = ('All',)
def create(self, request, format=None):
data = request.data
data['user'] = request.user.id
data['goal'] = WorkoutGoal.objects.get(goal=data['goal']).id
block = serializers.BlockSerializer(data=data, context={'request': request})
if block.is_valid():
new_block = block.save()
return Response({'block': {'name': new_block.name, 'id': new_block.id}}, status=status.HTTP_201_CREATED)
else:
return Response(block.errors, status=status.HTTP_400_BAD_REQUEST)
class WorkoutGoalSerializer(serializers.ModelSerializer):
class Meta:
model = apps.get_model('backend', 'WorkoutGoal')
fields = ('goal',)
goal = serializers.SerializerMethodField(read_only=True, source='get_goal')
def get_goal(self, obj):
return dict(WorkoutGoal.GOALS).get(obj.goal)
class BlockSerializer(serializers.ModelSerializer):
workout_count = serializers.IntegerField(required=False)
completed_workouts = serializers.IntegerField(required=False)
goal = WorkoutGoalSerializer()
class Meta:
model = apps.get_model('backend', 'Block')
read_only_fields = ('workout_count', 'completed_workouts')
fields = read_only_fields + ('id', 'name', 'user', 'created', 'goal')
The above code returns the correct choice, but I can't save under it. Remove the goal = WorkoutGoalSerializer() and it saves but doesn't return the mapped choice.
I think this will work like a charm,
class WorkoutGoalSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'request' in self.context and self.context['request'].method == 'GET':
self.fields['goal'] = serializers.SerializerMethodField(read_only=True, source='get_goal')
class Meta:
model = apps.get_model('backend', 'WorkoutGoal')
fields = ('goal',)
goal = serializers.SerializerMethodField(read_only=True, source='get_goal') # remove this line
def get_goal(self, obj):
return dict(WorkoutGoal.GOALS).get(obj.goal)
How this Work?
It will re-initiate the goal field with SerializerMethodField, if the reuested method is GET.
Remember one thing, you should remove the line,
goal = serializers.SerializerMethodField(read_only=True, source='get_goal')
serializers.py
class BlockCreateSerializer(serializers.ModelSerializer):
workout_count = serializers.IntegerField(required=False)
completed_workouts = serializers.IntegerField(required=False)
class Meta:
model = apps.get_model('backend', 'Block')
read_only_fields = ('workout_count', 'completed_workouts')
fields = read_only_fields + ('id', 'name', 'user', 'created', 'goal')
class BlockSerializer(serializers.ModelSerializer):
workout_count = serializers.IntegerField(required=False)
completed_workouts = serializers.IntegerField(required=False)
goal = WorkoutGoalSerializer()
class Meta:
model = apps.get_model('backend', 'Block')
read_only_fields = ('workout_count', 'completed_workouts')
fields = read_only_fields + ('id', 'name', 'user', 'created', 'goal')
views.py
class BlockViewSet(ModelViewSet):
model = apps.get_model('backend', 'Block')
queryset = model.objects.all()
serializer_class = serializers.BlockSerializer
permissions = ('All',)
def get_serializer_class(self):
if self.action == 'create':
return serializers.BlockCreateSerializer
else:
return self.serializer_class
def create(self, request, format=None):
data = request.data
data['user'] = request.user.id
data['goal'] = WorkoutGoal.objects.get(goal=data['goal']).id
block = self.get_serializer(data=data)
if block.is_valid():
new_block = block.save()
return Response({'block': {'name': new_block.name, 'id': new_block.id}}, status=status.HTTP_201_CREATED)
else:
return Response(block.errors, status=status.HTTP_400_BAD_REQUEST)
override get_serializer_class to return different serializer_class for create and other action(list\retrieve\update\partial_update)

DRF: Manipulating serializer field layout

I have a model that represents a house:
class House(models.Model):
name = models.CharField(...)
long = models.FloatField(...)
lat = models.FloatField(...)
and a serializer to return a list of houses in their most basic representation:
class HouseSerializer(serializers.ModelSerializer):
class Meta:
model = House
fields = ('id', 'name')
and the view
class HouseList(generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
this works fine. I can visit /api/house/ and I see a json list of houses:
{
'id': 1,
'name': 'Big House'
},
{
'id': 1
'name': 'Small House',
}...
Now I want to create a second view/resource at /api/maps/markers/ that returns my houses as a list of Google-Map-Friendly markers of the format:
{
'id': 1,
'long': ...,
'lat': ...,
'houseInfo': {
'title': "Big House",
}
} ...
I can foresee two approaches:
perform this as a separate serializer (using the same view as before) and mapping out the alternative field layout.
perform this as a separate view (using the same serializer as before) and simply layout the fields before creating a Response
but in neither approach am I clear on how to go about it nor which approach is preferable?
Answer 1
Looks to me like you need both - different view and serializer.
Simply because the view endpoint is not a sub-url of the first one, so they are not related - different view, even if they use the same model.
And different serializer - since you have a different field layout.
Not really sure how complicated is your case, but any code duplication can probably be solved by mixins anyway.
Answer 2
Depending on the use case:
if you also need to write data using the same struct, you need to define your own field class and handle the parsing correctly
if it's just reading data, you should be fine with this:
class HouseGoogleSerializer(HouseSerializer):
houseInfo = serializers.SerializerMethodField('get_house_info')
class Meta:
model = House
fields = [...]
def get_house_info(self, obj):
return {'title': obj.name}
where HouseSerializer is your base house serializer.
this code come from a running project and offer somethig more that you ask
but can easily adapted for your need if you want remove some features.
The current implemetation allow you:
use only one url one serializer and one view
choose the output using query string param (?serializer=std)
how to use in your code:
Case 1 (one url with ability to choose the serializer via querystring)
class HouseSerializer(HouseSerializer):
houseInfo = serializers.SerializerMethodField('get_house_info')
class Meta:
model = House
def get_house_info(self, obj):
return {'title': obj.name}
class HouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'name'),
'google' : ('id', 'long', 'lat', 'houseInfo')}
Case 2 (different views)
class HouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'name')}
class GoogleHouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'long', 'lat', 'houseInfo')}
==============
def serializer_factory(model, base=BaseHyperlinkedModelSerializer,
fields=None, exclude=None):
attrs = {'model': model}
if fields is not None:
attrs['fields'] = fields
if exclude is not None:
attrs['exclude'] = exclude
parent = (object,)
if hasattr(base, 'Meta'):
parent = (base.Meta, object)
Meta = type(str('Meta'), parent, attrs)
if model:
class_name = model.__name__ + 'Serializer'
else:
class_name = 'Serializer'
return type(base)(class_name, (base,), {'Meta': Meta, })
class DynamicSerializerMixin(object):
"""
Mixin that allow to limit the fields returned
by the serializer.
Es.
class User(models.Model):
country = models.ForeignKey(country)
username = models.CharField(max_length=100)
email = models.EmailField()
class UserSerializer(BaseHyperlinkedModelSerializer):
country = serializers.Field(source='country.name')
class MyViewSet(DynamicSerializerViewSetMixin, BaseModelViewSet):
model = User
serializer_class = UserSerializer
serializers_fieldsets = {'std': None,
'brief' : ('username', 'email')
}
this allow calls like
/api/v1/user/?serializer=brief
"""
serializers_fieldsets = {'std': None}
serializer_class = ModelSerializer
def get_serializer_class(self):
ser = self.request.QUERY_PARAMS.get('serializer', 'std')
fields = self.serializers_fieldsets.get(ser, 'std')
return serializer_factory(self.model,
self.serializer_class,
fields=fields)

django rest framework create nested objects "Models" by POST

I'm trying POST a new a Nested Object, the problem is just create the "top" object (Playlist), but don't create the "ChannelItem"...
My Models:
class Playlist(models.Model):
provider = models.IntegerField()
channel_id = models.CharField(max_length=100)
channel_version = models.CharField(blank=True, max_length=100)
start = models.DateTimeField()
url = models.CharField(max_length=500)
class ChannelItem(models.Model):
playlist = models.ForeignKey(Playlist, editable=False, related_name='channelitems')
content_id = models.CharField(max_length=100)
content_version = models.CharField(blank=True, max_length=100)
My Serializer:
class ChannelItemSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelItem
fields = ('content_id', 'content_version')
exclude = ('id')
depth = 1
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ('id', 'provider', 'channel_id', 'channel_version', 'start',
'url', 'channelitems')
depth = 2
channelitems = ChannelItemSerializer()
I use the curl to post the following data :
'{"provider":125,"channel_id":"xyz", "channel_version":"xsqt",
"start":"2012-12-17T11:04:35","url":"http://192.168.1.83:8080/maaaaa",
"channelitems":[{"content_id":"0.flv", "content_version":"ss"},
{"content_id":"1.flv","content_version":"ss"}]}' http://localhost:8000/playlist_scheduler/playlists/
I receive the message:
HTTP/1.1 201 CREATED
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 17 Dec 2012 20:12:54 GMT
Server: 0.0.0.0
{"id": 25, "provider": 125, "channel_id": "xyz", "channel_version": "xsqt",
"start":"2012-12-17T11:04:35", "url": "http://localhost:8080/something",
"channelitems": []}
Nested representations do not currently support read-write, and should instead be read-only.
You should probably look into using a flat representation instead, using pk or hyperlinked relations.
If you need the nested representation, you may want to consider having two separate endpoints - a flat writable endpoint, and a nested read-only endpoint.
If someone needs a quick-and-dirty solution for that, I came up with this one I'll be temporary using in a project:
class NestedManyToManyField(serializers.WritableField):
def to_native(self, value):
serializer = self.Meta.serializer(value.all(), many=True, context=self.context)
return serializer.data
def from_native(self, data):
serializer = self.Meta.serializer(data=data, many=True, context=self.context)
serializer.is_valid()
serializer.save()
return serializer.object
class Meta:
serializer = None
Then create your own subclass of NestedManyToManyField:
class TopicNestedSerializer(NestedManyToManyField):
class Meta:
serializer = MyOriginalSerializer
An example of MyOriginalSerializer:
class MyOriginalSerializer(serializers.ModelSerializer):
class Meta:
model = models.MyModel
fields = ('id', 'title',)
This works fine for me so far. But be aware there are clean fixes coming:
https://github.com/tomchristie/django-rest-framework/issues/960
https://github.com/tomchristie/django-rest-framework/pull/817
after a long effort I made a first version that funcinasse ...
I believe that with some improvement could be included within the ModelSerializer
class ChannelItemSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelItem
fields = ('id', 'content_id', 'content_version')
def field_from_native(self, data, files, field_name, into):
try:
if self._use_files:
_files = files[field_name]
else:
_data = data[field_name]
except KeyError:
if getattr(self, 'default', None):
_data = self.default
else:
if getattr(self, 'required', None):
raise ValidationError(self.error_messages['required'])
return
if type(_data) is list:
into[field_name] = []
for item in _data:
into[field_name].append(self._custom_from_native(item))
else:
into[field_name] = self._custom_from_native(_data)
def _custom_from_native(self, data):
self._errors = {}
if data is not None:
attrs = self.restore_fields(data, None)
attrs = self.perform_validation(attrs)
else:
self._errors['non_field_errors'] = ['No input provided']
if not self._errors:
return self.restore_object(attrs, instance=getattr(self, 'object', None))
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ('id', 'provider', 'channel_id', 'channel_version', 'start', 'url', 'channel_items')
depth = 1
channel_items = ChannelItemSerializer()
def restore_object(self, attrs, instance=None):
self.foreign_data = {}
for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.foreign_data[field_name] = attrs.pop(field_name)
return super(PlaylistSerializer, self).restore_object(attrs, instance)
def save(self, save_m2m=True):
super(PlaylistSerializer, self).save(save_m2m)
if getattr(self, 'foreign_data', None):
for accessor_name, object_list in self.foreign_data.items():
setattr(self.object, accessor_name, object_list)
self.foreign_data = {}
return self.object
For me, I have a hybrid workaround that I'm OK with. Namely, create a view that has:
the ManyToMany field in its un-nested serializer form
alias the nested ManyToMany field into a variable with _objs as the suffix and specify it as as read only
when you PUT back to the server reconcile the two aliased fields and store the result in the un-nested serializer field
e.g.
class MSerializer(serializers.HyperlinkedModelSerializer):
foo_objs = TempSensorSerializer(source='foos', many=True, allow_add_remove=True,required=False,read_only=True)
class Meta:
model = M
fields = ('url', 'foos', 'foo_objs')
I don't love this solution, but it beats trying to separately query and collate the nested fields after retrieving the initial container.