Make BooleanField required in Django Rest Framework - django

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)

Related

Django Test Client sends values as a list instead of strings

I have a problem, I am not sure whether I had overlooked something, or simply doing something wrong. I am trying to test an endpoint that allows a user to register.
My model:
class Account(User):
objects = UserManager()
balance = models.FloatField(blank=True, default=0)
rank_position = models.IntegerField(blank=True, null=True)
rank_title = models.CharField(max_length=255, blank=True, default="Novice")
Serializer:
class AccountSerializer(ModelSerializer):
class Meta:
model = Account
fields = '__all__
View:
#api_view(['POST'])
def register(request):
try:
acc = Account.objects.create(**request.POST)
acc_srl = AccountSerializer(acc)
return Response(data=acc_srl.data, status=status.HTTP_201_CREATED)
except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST)
And I am trying to use a Django test client in a following way:
class TestAuthentication(TestCase):
def setUp(self):
self.c = Client()
def test_register(self):
data = {'username': 'test_user', 'password': '1234'}
response = self.c.post('/api/register/', data)
print(response.json())
self.assertEqual(response.status_code, 201)
acc = Account.objects.get(username="test_user")
self.assertEqual(acc.username, "test_user")
self.assertTrue(isinstance(acc, User))
The function works as expected, but a strange thing happens. When I inspect request.POST both username and password are a list as so:
<QueryDict: {'username': ['test_user'], 'password': ['1234']}>
I am puzzled as I dont understand what causes this behavior.
This is a function built-in to Django to handle multiple values with the same key. See docs.
And when you are using Django's test client this.c.post and send data in the second parameter. Then Django will send that as URL parameters and not a POST body.
So your request will look like: '/api/register/?username=test_user&password=1234'
Let's say you send a request with '/api/register/?username=test_user&password=1234&password=5486'
Then your request.POST would look like:
<QueryDict: {'username': ['test_user'], 'password': ['1234', '5486']}>
So, I don't think you have to worry about this.
I think that's the normal behavior of the request.POST. As you can do a POST with multiple values for the same parameter: Eg: /api/register/?username=user&username=admin, so you will have
<QueryDict: {'username': ['user', 'admin']}>
You can check this in the official doc: https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.QueryDict
As mentioned in Django's doc:
In an HttpRequest object, the GET and POST attributes are instances of
django.http.QueryDict, a dictionary-like class customized to deal with
multiple values for the same key.
Then, if you want to access those items, use QueryDict.__getitem__ method:
So, in your case, it would be something like:
>>> request.POST['username']
test_user
>>> request.POST['password']
1234
Or if you have multiple values on the queryparams, you can use QueryDict.getlist:
>>> POST = QueryDict('user_ids=ID1&user_ids=ID2&user_ids=ID3&')
>>> POST.getlist('user_ids')
['ID1', 'ID2', 'ID3']
QueryDict: https://docs.djangoproject.com/en/3.1/ref/request-response/#querydict-objects
QueryDict.__getitem__: https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.QueryDict.\_\_getitem\_\_
QueryDict.getlist: https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.QueryDict.getlist

Django rest framework - Serializer always returns empty object ({})

I am creating first rest api in django using django rest framework
I am unable to get object in json format. Serializer always returns empty object {}
models.py
class Shop(models.Model):
shop_id = models.PositiveIntegerField(unique=True)
name = models.CharField(max_length=1000)
address = models.CharField(max_length=4000)
serializers.py
class ShopSerializer(serializers.ModelSerializer):
class Meta:
model = Shop
fields = '__all__'
views.py
#api_view(['GET'])
def auth(request):
username = request.data['username']
password = request.data['password']
statusCode = status.HTTP_200_OK
try:
user = authenticate(username=username, password=password)
if user:
if user.is_active:
context_data = request.data
shop = model_to_dict(Shop.objects.get(retailer_id = username))
shop_serializer = ShopSerializer(data=shop)
if shop:
try:
if shop_serializer.is_valid():
print('is valid')
print(shop_serializer.data)
context_data = shop_serializer.data
else:
print('is invalid')
print(shop_serializer.errors)
except Exception as e:
print(e)
else:
print('false')
else:
pass
else:
context_data = {
"Error": {
"status": 401,
"message": "Invalid credentials",
}
}
statusCode = status.HTTP_401_UNAUTHORIZED
except Exception as e:
pass
return Response(context_data, status=statusCode)
When i try to print print(shop_data) it always returns empty object
Any help, why object is empty rather than returning Shop object in json format?
Edited:
I have updated the code with below suggestions mentioned. But now, when shop_serializer.is_valid() is executed i get below error
{'shop_id': [ErrorDetail(string='shop with this shop shop_id already exists.', code='unique')]}
With the error it seems it is trying to update the record but it should only get the record and serialize it into json.
You're using a standard Serializer class in this code fragment:
class ShopSerializer(serializers.Serializer):
class Meta:
model = Shop
fields = '__all__'
This class won't read the contend of the Meta subclass and won't populate itself with fields matching the model class. You probably meant to use ModelSerializer instead.
If you really want to use the Serializer class here, you need to populate it with correct fields on your own.
.data - Only available after calling is_valid(), Try to check if serializer is valid than take it's data

Handling multiple query params

I have a create user view and here I first register a normal user and then create a player object for that user which has a fk relation with the user.
In my case, I have three different types of users
I created a view to handle register all three different types of users, but my player user has a lot of extra model fields and storing all query params in variables will make it messy.
Is there a better way to handle this, including validation?
TLDR; I created a view to handle register all three different types of users, but my player user has a lot of extra model fields and storing all query params in variables will make it messy. Is there a better way to handle this, including validation?
This is my view.
class CreateUser(APIView):
"""
Creates the User.
"""
def post(self, request):
email = request.data.get('email', None).strip()
password = request.data.get('password', None).strip()
name = request.data.get('name', None).strip()
phone = request.data.get('phone', None)
kind = request.query_params.get('kind', None).strip()
print(kind)
serializer = UserSerializer(data={'email': email, 'password':password})
serializer.is_valid(raise_exception=True)
try:
User.objects.create_user(email=email,
password=password)
user_obj = User.objects.get(email=email)
except:
raise ValidationError('User already exists')
if kind == 'academy':
Academy.objects.create(email=email, name=name, phone=phone, user=user_obj)
if kind == 'coach':
Coach.objects.create(email=email, name=name, phone=phone, user=user_obj)
if kind == 'player':
Player.objects.create(----------)
return Response(status=200)
Use a Model Serializer
In your case, define it in serializers.py like this:
from rest_framework import serializers
class CustomBaseSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['user'] = self.context['user']
return super(CustomBaseSerializer, self).create(validated_data)
class PlayerSerializer(CustomBaseSerializer):
class Meta:
model = Player
fields = ('count', 'height', 'right_handed', 'location',
'size', 'benchmark_swingspeed',
'benchmark_impactspeed', 'benchmark_stance',
'benchmark_balanceindex',)
class AcademySerializer(CustomBaseSerializer):
class Meta:
model = Academy
fields = '__all__' # Usually better to explicitly list fields
class CoachSerializer(CustomBaseSerializer):
class Meta:
model = Coach
fields = '__all__'
Then in your view
class CreateUser(APIView):
"""
Creates the User.
"""
def post(self, request):
print(kind)
try:
user = User.objects.get(email=request.data.get('email'))
except User.DoesNotExist:
pass
else:
raise ValidationError('User already exists')
user_serializer = UserSerializer(data=request.data)
user_serializer.is_valid(raise_exception=True)
user = user_serializer.save()
if kind == 'academy':
serializer_class = AcademySerializer
if kind == 'coach':
serializer_class = CoachSerializer
if kind == 'player':
serializer_class = PlayerSerializer
serializer = serializer_class(data=request.data, context={'user': user})
serializer.save()
return Response(serializer.data) # Status is 200 by default so you don't need to include it. RESTful API's should return the instance created, this also delivers the newly generated primary key back to the client.
# Oh and if you do serialize the object in the response, write serializers for academy and coach too, so the api response is consistent
Serializers are really powerful and useful. It is well worth thoroughly reading the docs.
First, I'd recommend to POST the parameters in a JSON or a form, instead of using the query params. But regardless the method, the solution is pretty much the same.
First, you could define the fields you're interested in a list. For example:
FIELDS = (
'count',
'height',
'right_handed',
'location',
'size',
'benchmark_swingspeed',
'benchmark_impactspeed',
'benchmark_stance',
'benchmark_balanceindex',
)
And then get all the values from the query params and store them in a dict, like:
player_params = {}
for field in FIELDS:
player_params[field] = request.query_params.get(field)
Now you have all the params required for a player in a dict and you can pass it to the Player model as **kwargs. Of course you'll probably need some validation. But in the end, you'll be able to do the following:
Player.objects.create(user=user_obj, **player_params)

DRF: accessing a SerializerMethodField during serializer validation

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.

How to filter objects by user id with tastypie?

I have the following user resource:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
fields = ['username', 'first_name', 'last_name']
allowed_methods = ['get']
filtering = {
'username': ALL,
'id': ALL,
}
and the following model resource:
class GoalResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
#authentication = BasicAuthentication()
#authorization = ReadOnlyAuthorization()
queryset = Goal.objects.all()
resource_name = 'goal'
filtering = {
'user': ALL_WITH_RELATIONS,
}
I want to be able to filter the goal by user id rather than username.
I can get a list of goals from certain usernames by doing a GET request on this:
http://localhost:8000/api/v1/goal/?user__username=test
But I want to be able to sort by user id instead:
http://localhost:8000/api/v1/goal/?user__id=1
How would I get the second part to work?
Also, what is the general procedure for accessing a currently logged in user's id through Javascript? I am using backbonejs, and I want to do a post for all of a logged in user's goal. I thought about putting a hidden field on the page with the user's id. Then extracting the value of the hidden field from the DOM, but I figured it's easy to use chrome's developer tools to change the id whenever I want. Of course, I will use authentication to check that the logged in user's id matches the one that I extract from the hidden field, though. But what is the accepted way?
I am not sure if what I propose here can work in your authorization. It works for me using ApiKeyAuthorization and Authorization.
I read the idea from:
http://django-tastypie.readthedocs.org/en/latest/cookbook.html [Section: Creating per-user resources ]
My suggestion is:
What about uncommenting authentication and authorization, and overriding obj_create and apply_authorization. I am using that in my project, and it works. In the code of the method apply_authorization, I just added the if condition checking for superuser, you can just return the object_list+filter without checking that (I do it cause if is not superuser, I return data related to groups of users).
class GoalResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
authentication = BasicAuthentication()
authorization = ReadOnlyAuthorization()
queryset = Goal.objects.all()
resource_name = 'goal'
filtering = {
'user': ALL_WITH_RELATIONS,
}
def obj_create(self, bundle, request=None, **kwargs):
return super(EnvironmentResource, self).obj_create(bundle, request, user=request.user)
def apply_authorization_limits(self, request, object_list):
if request.user.is_superuser:
return object_list.filter(user__id=request.GET.get('user__id',''))
Hope is what you were asking, and it helps.
best with that!
Note - apply_authorization_limits is deprecated.
The alternative way to filter by the current user, is to override read_list in you authorization class. This is what I have. My class overrides DjangoAuthorization.
def read_list(self, object_list, bundle):
klass = self.base_checks(bundle.request, object_list.model)
if klass is False:
return []
# GET-style methods are always allowed.
# Filter by user
if not hasattr(bundle.request, 'user'):
return None
object_list = object_list.filter(user__id=bundle.request.user.id)
return object_list