Validation of the reference field in mongoengine serializer - django

I'm using django with mongoengine and mongoengine-rest-framework.
As shown in this article, specifying related_model_validations field in Meta class of a Serializer
class Comment(Document):
post = ReferenceField(Post)
owner = ReferenceField(User)
text = StringField(max_length=140)
isApproved = BooleanField(default=False)
from rest_framework_mongoengine import mongoengine_serializer
class CommentSerializer(MongoEngineModelSerializer):
class Meta:
model = Comment
depth = 1
related_model_validations = {'owner': User, 'post': Post}
exclude = ('isApproved',)
can help to achieve the following result if the document referenced by the ReferenceField is missing:
{
"owner":["User with PK ... does not exist."]
}
So instead of raising a validation exception, json is modified.
However, this article is written for the old version of mongoengine-rest-framework and in the current version there is no field related_model_validations in Serializer Meta class.
So how to achieve the similar result in the current version of the mongoengine-rest-framework?

Sorry for late response, Aleksei.
Currently, if you want to PUT or POST a comment JSON, you just pass existing owner and post as their ids like:
{
post: 2,
user: aleksei.rozhnov#stackoverflow.com,
text: "Contrary to the popular belief, Karl Marx and Friedrich Engels are not a couple, but four different people"
}
So, if you want to update Comment, Post and Author at the same time, I'm afraid, that's not possible.
In GET requests you can get related fields as nested sub-JSONs with non-zero depth argument to Serializer, as you did it in your example.

Related

Django DRF serializer Custom relational fields How to build return value of to_internal_value?

I'm trying to implement something very similar to Djang Rest Framework tutorial Custom relational fields.
For the reminder, the provided code snippet is:
import time
class TrackListingField(serializers.RelatedField):
def to_representation(self, value):
duration = time.strftime('%M:%S', time.gmtime(value.duration))
return 'Track %d: %s (%s)' % (value.order, value.name, duration)
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackListingField(many=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
And "This custom field would then serialize to the following representation" (quoted in the tutorial):
{
'album_name': 'Sometimes I Wish We Were an Eagle',
'artist': 'Bill Callahan',
'tracks': [
'Track 1: Jim Cain (04:39)',
'Track 2: Eid Ma Clack Shaw (04:19)',
'Track 3: The Wind and the Dove (04:34)',
...
]
}
I understand that and have implemented it for my particular case.
What I don't understand is the way to implement to_internal_value(self, data) as I want to provide a read-write API.
I understand that to_internal_value(self, data) should return an AlbumTrack object, but I don't understand how to build it. In particular how to get back the Album related id?
If we post the JSON structure above, to_internal_value(self, data) will be called once per track with 'Track 1: Jim Cain (04:39)'... for data values. I don't see how we can update the tracks model from those data values.
It seems that you're trying to implement writable nested serializers. While nested serializers are read-only by default, the DRF has a section that explains how to implement writable ones: https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
Since you want the TrackListingField to serialize the Track model it should inherit from ModelSerializer:
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['order', 'name', 'duration']
You'll then have to override the create method for AlbumSerializer:
def create(self, validated_data):
tracks_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data)
for track_data in tracks_data:
Track.objects.create(album=album, **track_data)
return album
Please note that the above will make one database query per track. You can make use of Track.objects.bulk_create to make only one query to create all tracks.
To answer your initial question about to_internal_value, you can see what the default is by adding this print statement to the overridden to_internal_value:
class TrackSerializer(serializers.ModelSerializer):
...
def to_internal_value(self, data):
default_return_value = super(TrackSerializer, self).to_internal_value(data)
print(default_return_value)
return default_return_value
In the case of a ModelSerializer the DRF uses an OrderedDict output for to_internal_value. Your custom to_internal_value would have to extract the order, name and duration from the data string using a regex, and put them in an OrderedDict. However in this case it'd probably be easier to use a dictionary as representation for the tracks.

id is not present in validate() and ListSerializer's update() Django Rest Framework

I'm learning and new to Django Rest Framework and I'm having an issue in serializer validations and ListSerializer update method.
I have an APIView class which handles the put request. I just wanted to have a custom validation and so I've overridden validate method in the serializer class.
From postman I'm sending a JSON data to this APIView Class.
Sample JASON data:
[
{
"id": 1,
"ip": "10.1.1.1",
"host_name": "hostname 1"
},
{
"id": 2,
"ip": "10.1.1.2",
"host_name": "hostname 2"
}
]
When I receive the data and do serializer.is_valid() it passes the flow to the overridden validate function. But in there when I check for the attrs argument, I get all the fields except the id. The key and value for id are not present. It shows None.
The same issue occurred to me when I was trying to override the update method in ListSerializer.
when I tried the below code in the ListSerializer's update method,
data_mapping = {item['id']: item for item in validated_data}
I got an error saying KeyError 'id'.
It seems it's not accepting id field and I'm not sure why! Please, someone explain this to me if I'm wrong anywhere.
Serializer class
from rest_framework import serializers
from .models import NoAccessDetails
class NoAccessDetailsListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data)
data_mapping = {data.id: data for data in instance}
#Here I'm getting KeyError ID
validated_data_mapping = {item['id']: item for item in validated_data}
return
class NoAccessDetailsSerializer(serializers.ModelSerializer):
class Meta:
model = NoAccessDetails
list_serializer_class = NoAccessDetailsListSerializer
fields = ("id", "ip", "host_name")
def validate(self, data):
id_val = data.get('id')
ip = data.get('ip')
host_name = data.get('host_name')
#here the id value is None
print('id val {} '.format(id_val))
return data
If I am understanding correctly, the issue is that you do not see the id field inside of validated_data. If so, I believe this is intentional in the framework:
https://github.com/encode/django-rest-framework/issues/2320
Basically, the id field is read_only by default. Let me know if you have questions that are not answered by Tom's response to that issue.
EDIT: Also feel free to share the higher level use case (what you are planning on doing with the ID inside of validation), and maybe we can offer alternative approaches.

How extract individual values from POST request in Django Model Form?

Would like to be able to access data in a post request directly as well as processing it in the normal way. First created form:
class TransactionForm(ModelForm):
class Meta:
model = Transaction
fields = ['dish', 'customer', 'grams', 'amount_payable']
('customer' is the pk of another model, Customer.)
Then process form:
#csrf_exempt
def create_transaction(request):
print(request.POST)
user_input = TransactionForm(request.POST)
print (user_input)
if user_input.is_valid():
user_input.save()
#customerobject = Customer.objects.get(pk= PK-TAKEN FROM POST)
#customerobject.account_balance -= (amount_payable TAKEN FROM POST)
#customerobject.save()
return HttpResponse('AOK~')
else:
return HttpResponse(user_input) #'ERROR: transaction not valid~')
Am struggling to correctly formulate the commented lines above. (The rest works fine.)
Would like to be able to extract the value 'customer' from the POST in order to find the customer. Then to extract the value 'amount_payable' from the POST in order to deduct it from the customer's balance.
Eventually stumbled upon the relevant command:
cust = user_input.cleaned_data.get('customer')
customerobject = Customer.objects.get(pk=cust.id)
customerobject.account_balance -= user_input.cleaned_data.get('amount_payable')
customerobject.save()
Low-level languages are easier for sieve-heads like me.

Django Rest Framework - Updating a foreign key

I am a bit frustrated with this problem using the Django Rest Framework:
I am using a viewset, with a custom serializer. This serializer has its depth set to 1. When i query this viewset I get the correct representation of data for example:
data = {
id: 1,
issue_name: 'This is a problem',
status: {
id: 3,
name: 'todo'
}
}
The problem comes in when I need to update the status. For example if I want to select another status for this issue, for example:
status_new = {
id: 4,
name: 'done'
}
I send the following PATCH back to the server, this is the output:
data = {
id: 1,
issue_name: 'This is a problem',
status: {
id: 4,
name: 'done'
}
}
However, the status does not get updated. Infact, it is not even a part of the validated_data dictionary. I have read that nested relations are read-only. Could someone please tell me what I need to do this in a simple way?
Would really be obliged.
Thanks in advance
As stated in the documentation, you will need to write your own create() and update() methods in your serializer to support writable nested data.
You will also need to explicitly add the status field instead of using the depth argument otherwise I believe it won't be automatically added to validated_data.
EDIT: Maybe I was a bit short on the details: what you want to do is override update in ModelIssueSerializer. This will basically intercept the PATCH/PUT requests on the serializer level. Then get the new status and assign it to the instance like this:
class StatusSerializer(serializers.ModelSerializer):
class Meta:
model = Status
class ModelIssueSerializer(serializers.ModelSerializer):
status = StatusSerializer()
# ...
def update(self, instance, validated_data):
status = validated_data.pop('status')
instance.status_id = status.id
# ... plus any other fields you may want to update
return instance
The reason I mentioned in the comment that you might need to add a StatusSerializer field is for getting status into validated_data. If I remember correctly, if you only use depth then nested objects might not get serialized inside the update() / create() methods (although I might be mistaken on that). In any case, adding the StatusSerializer field is just the explicit form of using depth=1
I usually use custom field for such cases.
class StatusField(serializers.Field):
def to_representation(self, value):
return StatusSerializer(value).data
def to_internal_value(self, data):
try:
return Status.objects.filter(id=data['id']).first()
except (AttributeError, KeyError):
pass
And then in main serializer:
class IssueSerializer(serializers.ModelSerializer):
status = StatusField()
class Meta:
model = MyIssueModel
fields = (
'issue_name',
'status',
)
I would assume that your models mimic your serializer's data. Also, I would assume that you have a one to many relation with the status(es) but you don't need to create them via the issue serializer, you have a different endpoint for that. In such a case, you might get away with a SlugRelatedField.
from rest_framework import serializers
class StatusSerializer(serializers.ModelSerializer):
class Meta:
model = MyStatusModel
fields = (
'id',
'status',
)
class IssueSerializer(serializers.ModelSerializer):
status = serializers.SlugRelatedField(slug_field='status', queryset=MyStatusModel.objects.all())
class Meta:
model = MyIssueModel
fields = (
'issue_name',
'status',
)
Another valid solution would be to leave here the foreign key value and deal with the display name on the front-end, via a ui-select or select2 component - the RESTfull approach: you are handling Issue objects which have references to Status objects. In an Angular front-end app, you would query all the statuses from the back-end on a specific route and then you will display the proper descriptive name based on the foreign key value form Issue.
Let me know how is this working out for you.

Filtering on DateTimeField with Django Rest Framework

I have a model with a DateTimeField:
class MyShell(models):
created = models.DateTimeField(auto_now=true)
I have an api linked to it using Django Rest Framework:
class ShellMessageFilter(django_filters.FilterSet):
created = django_filters.DateTimeFilter(name="created",lookup_type="gte")
class Meta:
model = ShellMessage
fields = ['created']
class ShellListViewSet(viewsets.ModelViewSet):
"""
List all ShellMessages
"""
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
filter_class = ShellMessageFilter
When I hit my API using the following URL it works perfectly:
http://127.0.0.1:8000/api/shell/?created=2014-07-17
# It returns all shell with a date greater than the one provided in URL
But, I want to do more than that by filtering base on a date and a time. I tried the following URL without success:
http://127.0.0.1:8000/api/shell/?created=2014-07-17T10:36:34.960Z
# It returns an empty array whereas there are items with a created field greater than 2014-07-17T10:36:34.960Z
If you guys know how to proceed... I don't find any good informations or example in django-filters documentation...
Simpler solution if you don't care about fractions of seconds: replace the "T" with space (%20):
http://127.0.0.1:8000/api/shell/?created=2014-07-17%2010:36:34
Worked for me.
This may not be what you want, but you could simply convert from Unix time. E.g.:
def filter_unix_dt(queryset, value):
if not value:
return queryset
try:
unix_time = int(value)
t = datetime.fromtimestamp(unix_time)
result = queryset.filter(created__gte=t)
return result
except ValueError:
return queryset
class ShellMessageFilter(django_filters.FilterSet):
created = django_filters.DateTimeFilter(action=filter_unix_dt)
class Meta:
model = ShellMessage
fields = ['created']
The issue and solution are documented in this DRF issue page: https://github.com/tomchristie/django-rest-framework/issues/1338
TL;DR: A Django ISO conversion 'issue' is preventing DRF from working as you are expecting. A fix for this has been written in DRF, allowing you to use IsoDateTimeField instead of DateTimeField. Just replaying the T with a space in your request param value also works.