I am trying to create nested objects (documentregulation) when I create a Document object. To achieve this, I have overwritten the create method on the DocumentSerializer, as per Django Docs. However, when I attempt validated_data.pop('documentregulation_set'), it is empty, even when it is populated in the incoming request.data of my view. Is there something causing my incoming data to not be validated? How would I go about debugging this if so?
// serializers.py
class DocumentRegulationSerializer(serializers.ModelSerializer):
class Meta:
model = DocumentRegulation
fields = ('regulation',)
class DocumentSerializer(serializers.ModelSerializer):
documentregulation_set = DocumentRegulationSerializer(many=True, required=False)
class Meta:
model = Document
fields = ('documentregulation_set', 'id', 'name', 'file', 'text', 'uploaded_at')
extra_kwargs = {
'id': {'read_only': True},
'uploaded_at': {'read_only': True},
}
def create(self, validated_data):
documentregulation_set = validated_data.pop('documentregulation_set')
# create document first
document = Document.objects.create(**validated_data)
# serialize associated regulations
for documentregulation in documentregulation_set:
# get ID of newly-created document, use for relation
#documentregulation['regulation'] = documentregulation['id']
DocumentRegulation.objects.create(document=document, **documentregulation)
return document
//views.py
class DocumentView(generics.ListCreateAPIView):
def create(self, request):
#pprint(self.request.FILES['profile_pic'])
request.data['documentregulation_set'] = json.loads(request.data['documentregulation_set'])
request.data['documentdomain_set'] = json.loads(request.data['documentdomain_set'])
pprint(request.data)
document = DocumentSerializer(data=request.data)
if document.is_valid():
document.save()
return Response(document.data, status=status.HTTP_201_CREATED)
else:
return Response(document.errors, status=status.HTTP_400_BAD_REQUEST)
my incoming data (printed in request.data) looks like:
{'documentregulation_set': [{'label': 'Regulation 1',
'regulation': 2,
'value': 2},
{'label': 'Regulation 2',
'regulation': 4,
'value': 4}],
'file': <InMemoryUploadedFile: test.docx >,
'name': 'testing',
'text': 'test'}
but then my validated data prints out to be:
{'documentregulation_set': [],
'file': <InMemoryUploadedFile: test.docx >,
'name': 'testing',
'text': 'test'}
Problem seems to be with the validation of the documentregulation_set field in the DocumentSerializer serializer. But you can escape the validation in the Meta Class as:
class Meta:
model = Document
fields = '__all__'
extra_kwargs = {
'documentregulation_set': {'validators': []} # escape validation
}
If you need to write custom validators have a look at Writing custom validators
So the final serializer looks like:
class DocumentRegulationSerializere(serializers.ModelSerializer):
"""Serializers for DocumentRegulation object"""
class Meta:
model = DocumentRegulation
fields = ('regulation',)
class DocumentSerializer(serializers.ModelSerializer):
"""Serializer for Document objects"""
documentregulation_set = DocumentRegulationSerializere(many=True)
class Meta:
model = Document
fields = ('documentregulation_set', 'id', 'name', 'file', 'text', 'uploaded_at')
extra_kwargs = {
'id': {'read_only': True},
'uploaded_at': {'read_only': True},
'documentregulation_set': {'validators': []} # escape validation
}
def create(self, validated_data):
doc_reg = []
document_regulation_set = validated_data.pop('documentregulation_set')
document = Document.objects.create(**validated_data)
for document_regulation in document_regulation_set:
reg = DocumentRegulation.objects.create(**document_regulation)
reg.save()
doc_reg.append(reg)
document.documentregulation_set.add(reg)
return document
View
class DocumentView(generics.ListCreateAPIView):
"""List or create new Document ojects"""
queryset = Document.objects.all()
serializer_class = DocumentSerializer
Related
I'm new to testing and I spent a day finding a solution for my problem but I couldn't find any.
this is my serializer
serilaizer.py
class LeadSerializer(serializers.ModelSerializer):
def create(self, validated_data):
user = self.context['user']
return Lead.objects.create(organizer=user.organizeruser, **validated_data)
class Meta:
model = Lead
fields = ['id', 'first_name', 'last_name', 'age', 'agent', 'category', 'description', 'date_added',
'phone_number', 'email', 'converted_date'
]
I have two types of users, organizer, and agent. organizer can create a lead but agent can't. and as you see I don't have organizer field. authenticated user will be added to the organizer field when a Lead is created.
test.py
def test_if_lead_exist_return_200(self, api_client, leads_factory, user_factory):
user = user_factory.create(is_organizer=True)
api_client.force_authenticate(user=User(is_staff=True))
lead = leads_factory.create()
serializer = LeadSerializer(context={'request': user})
print(serializer)
# here I can see the user
response = api_client.get(f'/api/leads/{lead.id}/', )
assert response.status_code == status.HTTP_200_OK
assert response.data == {
'id': lead.id,
'first_name': lead.first_name,
'last_name': lead.last_name,
'age': lead.age,
'organizer': lead.organizer.id,
'agent': lead.agent.id,
'category': lead.category.id,
'description': lead.description,
'date_added': lead.date_added,
'phone_number': lead.phone_number,
'email': lead.email,
'converted_date': lead.converted_date,
}
because there is no organizer field in the serialzier test won't pass and this is the result of the test
what can I do here? can I pass the organizer user to the response?
You should add the organizer into the fields.
class LeadSerializer(serializers.ModelSerializer):
class Meta:
model = Lead
# here I added the `organizer` field
fields = ['id', 'first_name', 'last_name', 'age', 'agent', 'category', 'description', 'date_added', 'phone_number', 'email', 'converted_date', 'organizer']
def create(self, validated_data):
...
For my application, I have Locations, and Events given by the following (simplified) models:
class Location(models.Model):
name = models.CharField(max_length=64)
class Event(models.Model):
location = models.ForeignKey(Location)
date = models.DateField()
In my API, I have an endpoint: /api/locations/ which returns all locations, and each location has its events embedded within it, e.g.:
[
{
'id': 1,
'name': 'Location 1',
'events': [
{'id': 1, 'date': '2018-01-01'},
{'id': 2, 'date': '2018-01-14'}
]
},
{
'id': 2,
'name': 'Location 2',
'events': [
{'id': 3, 'date': '2017-12-28'},
{'id': 4, 'date': '2018-01-10'}
]
}
]
my serializers look like:
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ('id', 'date',)
read_only_fields = ('id',)
class LocationSerializer(serializers.ModelSerializer):
events = EventSerializer(many=True)
class Meta:
model = Location
fields = ('id', 'name', 'events',)
read_only_fields = ('id',)
Using ViewSets, I would like to add a filter to this call, with a max_date, and min_date parameter, e.g.: /api/locations/?min_date=2018-01-01&max_date=2018-01-15. Which should return only locations that have events between those two dates, AND should only return the events for each location between those two dates.
In the example data above, both locations would return, Location 1 would have both events listed, but Location 2 would only have the second event in its list. In my ViewSet, I can take those parameters and append them as queryset filters:
class LocationViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = LocationSerializer
def get_queryset(self):
queryset = Location.objects.all()
min_date = self.request.query_params.get('min_date', None)
max_date = self.request.query_params.get('max_date', None)
if min_date is not None and max_date is not None:
queryset = queryset.filter(event__date__lte=max_date, event__date__gte=min_date)
return queryset
Which will filter out any locations that do not have any events between the two dates, however those returned locations show ALL of their events, not just the ones between min_date and max_date. I assume I have to somehow pass my query parameters on to the EventSerializer embedded within LocationSerializer, but am unsure as to how I might do this.
Try to prefetch filtered events in view:
from django.db.models import Prefetch
class LocationViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
serializer_class = LocationSerializer
def get_queryset(self):
queryset = Location.objects.all()
min_date = self.request.query_params.get('min_date', None)
max_date = self.request.query_params.get('max_date', None)
if min_date is not None and max_date is not None:
queryset = queryset.filter(event__date__lte=max_date, event__date__gte=min_date).prefetch_related(Prefetch('events', queryset=Event.objects.filter(date__lte=max_date, date__gte=min_date))
return queryset
Also you can use serializerMethodField to filter events:
class LocationSerializer(serializers.ModelSerializer):
events = SerializerMethodField()
class Meta:
model = Location
fields = ('id', 'name', 'events',)
read_only_fields = ('id',)
get_events(self, obj):
min_date = self.context['request'].query_params.get('min_date', None)
max_date = self.context['request'].query_params.get('max_date', None)
events = obj.events.filter(date__lte=max_date, date__gte=min_date)
return LocationSerializer(events, many=True).data
With self.context['request'] you can get request data inside serializer.
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': ...,
}]
}
I want users to be able to update only one specific field. For example:
models.py
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta:
ordering = ('created',)
serializer.py
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
views.py
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
Once the Snippet is created, the user should only be able to update title field.
I know I can achieve that by something like this:
serializers.py
def update(self, instance, validated_data):
"""
Update and return an existing `Snippet` instance, given the validated data.
"""
instance.title = validated_data.get('title', instance.title)
instance.save()
return instance
In serializer class. But I want to know, is there a way that browsable API show only title field in edit form? And also skip validation for fields that are not required?
Django REST Framework provides the read_only and write_only attributes for controlling what is used for editing and what is not.
serializers.py
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
extra_kwargs = {
'id': {'read_only': True},
'code': {'read_only': True},
'lineos': {'read_only': True},
'language': {'read_only': True},
'style': {'read_only': True}
}
The above will return all the fields on read requests but only title will be writable.
You can find more at the official documentation:
http://www.django-rest-framework.org/api-guide/serializers/#specifying-read-only-fields
While #petkostas answer is correct, it doesn't give you a full picture of how to achieve it.
First, Create a new serializer; let's call it SnippetUpdateSerializer
Now, you may have custom serializer fields like serializers.MethodFieldSerializer that you would have defined in SnipperSerializer; which you may not want to write again in your new serializer. A good approach is to use inheritance.
Taking the example from the question
class SnippetUpdateSerializer(SnippetSerializer): #<- pay attention here
class Meta(SnippetSerializer.Meta): # <- pay attention here
SnippetSerializer.Meta.extra_kwargs.update({ # update the dictionary
'id': {'read_only': True},
'code': {'read_only': True}, # you can also use {write_only: True} if you want this to be write only
'lineos': {'read_only': True},
'language': {'read_only': True},
'style': {'read_only': True}
}) # you may completely override by just using extra_kwargs, instead of SnippetSerializer.Meta.extra_kwargs
Now in your SnippetUpdateView, use the above serializer.
If you are using class based views then set serializer_class = SnippetUpdateSerializer
Another approach is to return bad request, from your update view if the user requests contain read_only fields. (not recommended)
in one of my admin forms, I override a model form field (location) in order to use a different, map specific form field (olwidget). This MapField should include a layer (InfoLayerField) that displays all other model instances but the one that is being edited at the moment. Right now, it displays all the model instances (see MyModel.objects.all()) which means, if a model is edited the current location is displayed twice.
In order to achieve this, I'd have to exclude the current edited model instance from the QuerySet used in InfoLayerField (something like MyModel.objects.exclude(pk=self.instance.pk)). But since form fields are defined as static variables, I can't access self.instance.
Is there any way to achieve this?
# models.py
class MyModel(models.Model):
name = models.CharField(max_length=200)
location = models.PointField(blank=True, null=True)
# admin.py
from olwidget.fields import MapField, EditableLayerField, InfoLayerField
from olwidget.utils import get_ewkt
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
location = MapField([
EditableLayerField({
'geometry': 'point',
'name': 'location',
}),
InfoLayerField(
[(get_ewkt(m.location), m.name) for m in MyModel.objects.all() if m.location ], {
'geometry': 'point',
'name': 'other locations',
'cluster': True,
'cluster_display': 'list',
}
)
])
class MyModelOlwidgetAdmin(admin.ModelAdmin, GeoModelAdmin):
form = MyModelAdminForm
...
Thanks for any hint's.
I think you just need to override the __init__ on your form:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self,*args,**kwargs):
super(MyModelAdminForm, self).__init__(*args,**kwargs)
qs = MyModel.objects.exclude(pk = self.instance.pk) #grab instance.pk here
self.fields['location'] = MapField([
EditableLayerField({
'geometry': 'point',
'name': 'location',
}),
InfoLayerField(
[(get_ewkt(m.location), m.name) for m in qs if m.location ], {
'geometry': 'point',
'name': 'other locations',
'cluster': True,
'cluster_display': 'list',
}
)
])