DRF: accessing a SerializerMethodField during serializer validation - django

I'm using Django Rest Framework 3.0 and I have a model:
class Vote(models.Model):
name = ...
token = models.CharField(max_length=50)
where token is a unique identifier that I generate from the request IP information to prevent the same user voting twice
I have a serializer:
class VoteSerializer(serializers.ModelSerializer):
name = ...
token = serializers.SerializerMethodField()
class Meta:
model = Vote
fields = ("id", "name", "token")
def validate(self, data):
if Rating.objects.filter(token=data['token'], name=data['name']).exists():
raise serializers.ValidationError("You have already voted for this")
return data
def get_token(self, request):
s = ''.join((self.context['request'].META['REMOTE_ADDR'], self.context['request'].META.get('HTTP_USER_AGENT', '')))
return md5(s).hexdigest()
and a CreateView
But I am getting a
KeyError: 'token'
when I try to post and create a new Vote. Why is the token field not included in the data when validating?
The docs mention:
It can be used to add any sort of data to the serialized representation of your object.
So I would have thought it would be also available during validate?

Investigating, it seems that SerializerMethodField fields are called after validation has occurred (without digging into the code, I don't know why this is - it seems counter intuitive).
I have instead moved the relevant code into the view (which actually makes more sense conceptually to be honest).
To get it working, I needed to do the following:
class VoteCreateView(generics.CreateAPIView):
serializer_class = VoteSerializer
def get_serializer(self, *args, **kwargs):
# kwarg.data is a request MergedDict which is immutable so we have
# to copy the data to a dict first before inserting our token
d = {}
for k, v in kwargs['data'].iteritems():
d[k] = v
d['token'] = self.get_token()
kwargs['data'] = d
return super(RatingCreateView, self).get_serializer(*args, **kwargs)
def get_token(self):
s = ''.join((self.request.META['REMOTE_ADDR'], self.request.META.get('HTTP_USER_AGENT', '')))
return md5(s).hexdigest()
I really hope this isn't the correct way to do this as it seems totally convoluted for what appears to be a pretty straight forward situation. Hopefully someone else can post a better approach to this.

Related

REST Django - Can't find context of request from within my validator

Please be gentle. I'm a Django newb and I find the level of abstraction just plain overwhelming.
My ultimate goal is to modify an image file on its way into the model. That part may or may not be relevant, but assistance came my way in this post which advised me that I should be making changes inside a validator:
REST Django - How to Modify a Serialized File Before it is Put Into Model
Anyway, at the moment I am simply trying to get the context of the request so I can be sure to do the things to the thing only when the request is a POST. However, inside my validator, the self.context is just an empty dictionary. Based on what I have found out there, there should be a value for self.context['request'].
Here is what I have:
Serializer with validator method:
class MediaSerializer(serializers.ModelSerializer):
class Meta:
model = Media
fields = '__all__'
def validate_media(self, data):
print(self.context)
#todo: why is self.context empty?
#if self.context['request'].method == 'POST':
# print('do a thing here')
return data
def to_representation(self, instance):
data = super(MediaSerializer, self).to_representation(instance)
return data
The view along with the post method
class MediaView(APIView):
queryset = Media.objects.all()
parser_classes = (MultiPartParser, FormParser)
permission_classes = [permissions.IsAuthenticated, ]
serializer_class = MediaSerializer
def post(self, request, *args, **kwargs):
user = self.request.user
print(user.username)
request.data.update({"username": user.username})
media_serializer = MediaSerializer(data=request.data)
# media_serializer.update('username', user.username)
if media_serializer .is_valid():
media_serializer.save()
return Response(media_serializer.data, status=status.HTTP_201_CREATED)
else:
print('error', media_serializer.errors)
return Response(media_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The Model:
class Media(models.Model):
objects = None
username = models.ForeignKey(User, to_field='username',
related_name="Upload_username",
on_delete=models.DO_NOTHING)
date = models.DateTimeField(auto_now_add=True)
#temp_media = models.FileField(upload_to='upload_temp', null=True)
media = models.FileField(upload_to='albumMedia', null=True)
#todo: potentially this will go to a temp folder, optimize will be called and then permananent home will be used -jjb
#MEDIA_ROOT path must be /src/upload
file_type = models.CharField(max_length=12)
MEDIA_TYPES = (
('I', "Image"),
('V', "Video")
)
media_type = models.CharField(max_length=1, choices=MEDIA_TYPES, default='I')
ACCESSIBILITY = (
('P', "Public"),
('T', "Tribe"),
('F', "Flagged")
)
user_access = models.CharField(max_length=1, choices=ACCESSIBILITY, default='P')
So I'm just trying to figure out how to fix this context problem. Plus if there are any other tips on how to get where I'm going, I'd be most appreciative.
PS I'm pretty new here. If I wrote this question in a way that is inappropriate for stack overflow, please be kind, and I will correct it. Thanks.
I don't think you need to worry about checking if the request is a POST inside the validate_media() method. Generally validation only occurs during POST, PATCH, and PUT requests. On top of that, validation only occurs when you call is_valid() on the serializer, often explicitly in a view, as you do in your post() function. As long as you never call is_valid() from anywhere other than post(), you know that it is a POST. Since you don't support patch() or put() in your view, then this shouldn't be a problem.
inside my validator, the self.context is just an empty dictionary
You must explicitly pass in context when creating a serializer for it to exist. There is no magic here. As you can see in the source code context defaults to {} when you don't pass it in.
To pass in context, you can do this:
context = {'request': request}
media_serializer = MediaSerializer(data=request.data, context=context)
Even better, just pass in the method:
context = {'method': request.method}
media_serializer = MediaSerializer(data=request.data, context=context)
You can make the context dictionary whatever you want.

How to share validation and schemas between a DRF FilterBackend and a Serializer

I am implementing some APIs using Django Rest Framework, and using the generateschema command to generate the OpenApi 3.0 specs afterwards.
While working on getting the schema to generate correctly, I realized that my code seemed to be duplicating a fair bit of logic between the FilterBackend and Serializer I was using. Both of them were accessing and validating the query parameters from the request.
I like the way of specifying the fields in the Serializer (NotesViewSetGetRequestSerializer in my case), and I would like to use that in my FilterBackend (NoteFilterBackend in my case). It would be nice to have access to the validated_data within the filter, and also be able to use the serializer to implement the filtering schemas.
Are there good solutions out there for only needing to specify your request query params once, and re-using with the filter and serializer?
I've reproduced a simplified version of my code below. I'm happy to provide more info on ResourceURNRelatedField if it's needed (it extends RelatedField and uses URNs instead of primary keys), but I think this would apply to any kind of field.
class NotesViewSet(generics.ListCreateAPIView, mixins.UpdateModelMixin):
allowed_methods = ("GET")
queryset = Note.objects.all()
filter_backends = [NoteFilterBackend]
serializer_class = NotesViewSetResponseSerializer
def get(self, request, *args, **kwargs):
query_params_dict = request.query_params
request_serializer = NotesViewSetGetRequestSerializer(data=query_params_dict)
request_serializer.is_valid(raise_exception=True)
validated_data = request_serializer.validated_data
member = validated_data.get("member_urn")
team = validated_data.get("team_urn")
if not provider_can_view_member(request.user, member, team):
return custom404(
request,
HttpResponseNotFound(
"Member does not exist!. URN={}".format(member.urn())
),
)
return super(NotesViewSet, self).list(request, *args, **kwargs)
class NotesViewSetGetRequestSerializer(serializers.Serializer):
member_urn = ResourceURNRelatedField(queryset=User.objects.all(), required=True)
team_urn = ResourceURNRelatedField(queryset=Team.objects.all(), required=True)
privacy_scope = serializers.CharField(required=False)
def validate_privacy_scope(self, value):
choices = dict(Note.PRIVACY_SCOPE_CHOICES)
if value and value not in choices:
raise serializers.ValidationError(
"bad privacy scope {}. Supported values are: {}".format(value, choices)
)
else:
return value
class NoteFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
member_urn = request.query_params.get("member_urn")
customer_uuid = URN.from_urn(member_urn).id
privacy_scope = request.query_params.get("privacy_scope")
team_urn = request.query_params.get("team_urn")
team_uuid = URN.from_urn(team_urn).id
queryset = queryset.filter(customer__uuid=customer_uuid)
if privacy_scope == Note.PRIVACY_SCOPE_TEAM_PROVIDERS:
queryset = queryset.filter(team__uuid=team_uuid)
return queryset

How to use SlugRelatedField for incoming request (deserialise) and full serialiser for response for related fields

My db model is of events and each event is connected to a venue.
When I retrieve a list of events I use:
venue = VenueSerializer(read_only=True)
When I post to my drf endpoint I use:
venue = serializers.SlugRelatedField(
allow_null=True,
queryset=Venue.objects.all(),
required=False,
slug_field='id')
However this causes that in the response I recieve from the post request, the venue is serialised as a slug field. I want it to use the VenueSerialiser for the response.
I came accross https://stackoverflow.com/a/49035208/5683904 but it only works on the Viewset itself.
#serializer_class = EventSerializer
read_serializer_class = EventSerializer
create_serializer_class = EventCreateUpdateSerializer
I need to build this functionality into the serialiser itself since it is shared with other components.
The Problem
The SlugRelatedField's to_representation method is coded to return the value of the slug_field keyword argument that you pass to it during initialization.
Workarounds
Extend SlugRelatedField and override it's to_representation method to return the complete object instead of the slug. This could be a little tricky because the actual model instance isn't a part of the class.
Have two fields, one for the slug and another for the actual object. This is way easier to implement.
Here's how you can implement the second workaround:
venue = VenueSerializer(read_only=True)
venue_id = serializers.SlugRelatedField(
write_only=True
allow_null=True,
queryset=Venue.objects.all(),
required=False,
slug_field='id')
UPDATE: This is apparently a pretty wanted feature in DRF. I've found a way to implement the first workaround as well. It deals with PrimaryKeyRelatedField but you could probably modify it to work with SlugRelatedField too. Here it is:
from collections import OrderedDict
from rest_framework import serializers
class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
return self.serializer_class(value).data
def get_queryset(self):
if self.queryset:
return self.queryset
return self.serializer_class.Meta.model.objects.all()
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
item.pk,
self.display_value(item)
)
for item in queryset
])
def use_pk_only_optimization(self):
return False
#classmethod
def from_serializer(cls, serializer, name=None, args=(), kwargs={}):
if name is None:
name = f"{serializer.__class__.name}AsymetricAutoField"
return type(name, [cls], {"serializer_class": serializer})
You can then use this field like this:
class FooSerializer(serilizers.ModelSerializer):
bar = AsymetricRelatedField(BarSerializer)
class Meta:
model = Foo
You can find the original discussion about this here

How to deserialize foreignkey with django rest framework?

For example
class People(models.Model):
name = models.CharField(max_length=20)
class Blog(models.Model):
author = models.ForeignKeyField(People)
content = models.TextField()
and then,
class CreateBlogSerializer(serializers.Serializer):
#author id
author = serializers.IntegerField()
content = serializers.TextField()
In views, I need to get author_id , check if the id exists and get the author instance, it's fussy to do that.
serializer = CreateBlogSerializer(data=request.DATA)
if serializer.is_valid():
try:
author = Author.objects.get(pk=serializer.data["author"])
except Author.DoesNotExist:
return Response(data={"author does not exist"})
blog = Blog.objects.create(author=author, content=serializer.data["content"])
Is there a ForeignKeyField to deserialize and validate primarykey data and then return a instance.
class CreateBlogSerializer(serializers.Serializer):
author = serializers.ForeignKeyField(Author)
content = serializers.TextField()
serializer = CreateBlogSerializer(data=request.DATA)
if serializer.is_valid():
#after deserialization , the author id becomes author model instance
blog = Blog.objects.create(author=serializer.data["author"], content=serializer.data["content"])
else:
#the author id does not exist will cause serializer.is_valid=Flase
PS
I knew PrimaryKeyRelatedField in ModelSerializer,but I can't use ModelSerializer here, the model structures are complex, the above are just examples。
My first thought is to write a customer field.
class ForeignKeyField(WritableField):
def __init__(self, model_name, *args, **kwargs):
super(ForeignKeyField, self).__init__(*args, **kwargs)
self.model_name = model_name
self.model_instance = None
def from_native(self, pk):
if not isinstance(pk, int):
raise ValidationError("pk must be int")
try:
self.model_instance = self.model_name.objects.get(pk=pk)
return self.model_instance
except self.model_name.DoesNotExist:
raise ValidationError('object does not exist')
def to_native(self, obj):
return self.model_instance
I hacked it, but I don't konw why it works.
Usage:
there is a little difference
class t_serializer(serializers.Serializer):
author = ForeignKeyField(Author)
#api_view(["POST"])
def func(request):
serializer = t_serializer(data=request.DATA)
if serializer.is_valid():
print isinstance(serializer.data["author"], Author)
#print True
else:
print serializer.errors
It appears that what you want is custom validation code.
For this particular instance, you could write the following.
class CreateBlogSerializer(serializers.Serializer):
author = serializers.ForeignKeyField(Author)
content = serializers.TextField()
def validate_author(self, attrs, source):
"""
Verify that the author exists.
"""
author = attrs[source]
if Author.objects.filter(pk=author).exists():
return attrs
raise serializers.ValidationError("Specified Author does not exist!")
Now when you call serializer.is_valid() this check will occur.
So you can then do this elsewhere in your code,
if serializer.is_valid():
blog = Blog.objects.create(author=serializer.data["author"], content=serializer.data["content"])
And be sure that if the given blog is created, there is a corresponding Author already in the DB.
Detailed Explanation
So Django Rest Framework provides a method of adding custom validation to serializers. This can be done with by providing methods of the following format in the serializer class validate_<field name>. These methods will set the source value to name of the given field, in our example author, which can then be used with the attrs variable to get the current value of the field (attrs contains all the values passed into the serializer).
These validation methods are each called with then serializer.is_valid() method is called. They are supposed to just return attrs if the given test passes, and raise a ValidationError otherwise.
They can get somewhat complex, so it is probably best if you read the full documentation here, http://www.django-rest-framework.org/api-guide/serializers#validation

Make BooleanField required in Django Rest Framework

I've got a model with a boolean field that I'd like to deserialize with the Django rest framework and I want the serializer to complain when a field is missing in the post request. Yet, it doesn't. It silently interprets a missing boolean as False.
class UserProfile(models.Model):
"""
Message between two users
"""
user = models.OneToOneField(User, verbose_name="django authentication user", related_name='user_profile')
newsletter = models.BooleanField(null=False)
research = models.BooleanField(null=False)
The model is created with a Serialiser like this:
class UserProfileSerializer(serializers.ModelSerializer):
research = BooleanField(source='research', required=True)
newsletter = BooleanField(source='newsletter', required=True)
class Meta:
model = UserProfile
fields = ('research', 'newsletter')
In my view I'm also creating a user, so I have some manual steps:
def post(self, request, format=None):
userprofile_serializer = UserProfileSerializer(data=request.DATA)
reg_serializer = RegistrationSerializer(data=request.DATA)
phone_serializer = PhoneSerializer(data=request.DATA)
errors = {}
if userprofile_serializer.is_valid() and reg_serializer.is_valid() and phone_serializer.is_valid():
user = reg_serializer.save()
data = reg_serializer.data
user_profile = userprofile_serializer.object
user_profile.user = user
userprofile_serializer.save()
return Response(data, status=status.HTTP_201_CREATED)
errors.update(reg_serializer.errors)
# ...
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
However, the following test case fails, because the rest framework doesn't complain about the missing param but instead inserts a False in from_native
def test_error_missing_flag(self):
data = {'username': "test", 'password': "123test", 'email': 'test#me.com',
'newsletter': 'true', 'uuid': self.uuid}
response = self.client.post(reverse('app_register'), data)
# should complain that 'research' is not found
self.assertTrue('research' in response.data)
If I replace my 'research' field with an Integer field that the serializer fails as expected. Any ideas?
There was an issue with Boolean fields and the required argument. Should now be fixed in master.
See this issue: https://github.com/tomchristie/django-rest-framework/issues/1004
Add
your_field = serializers.NullBooleanField(required=False)
in serializer.
That's it. It'll work :)
For anyone who has read #Tom's accepted answer from 2013 and finds that this still doesn't work, it's because this behavior is intended for HTML form inputs. Here's the original issue.
To use serializers.BooleanField with a JSON payload, convert your request.POST to a Python dict by doing request.POST.dict() and pass it to your serializer while initializing.
Create a new custom class:
from rest_framework import serializers
class RequirableBooleanField(serializers.BooleanField):
default_empty_html = serializers.empty
Now, you can use:
research = RequirableBooleanField(required=True)
or
research = RequirableBooleanField(required=False)