DRF: Custom field error message - python

While creating simple login api using DRF, I encountered a problem. Two field email and password are required to login. If the fields are left blank following json message is shown:
{
"email": [
"This field may not be blank."
],
"password": [
"This field may not be blank."
]
}
But I would like to customise the error message, say to something like,
{
"email": [
"Email field may not be blank."
],
"password": [
"Password field may not be blank."
]
}
I tried the something like the following in validate() in serializers.py :
if email is None:
raise serializers.ValidationError(
'An email address is required to log in.'
)
But it is not getting override, I'm not sure about the reason.
Edit
I implemented with #dima answer it still not work. What am I doing wrong?, now my serializer looks like:
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255, required=True, error_messages={"required": "Email field may not be blank."})
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True, required=True,
error_messages={"required": "Password field may not be blank."})
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
# The `validate` method is where we make sure that the current
# instance of `LoginSerializer` has "valid". In the case of logging a
# user in, this means validating that they've provided an email
# and password and that this combination matches one of the users in
# our database.
email = data.get('email', None)
password = data.get('password', None)
user = authenticate(username=email, password=password)
# If no user was found matching this email/password combination then
# `authenticate` will return `None`. Raise an exception in this case.
if user is None:
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
# Django provides a flag on our `User` model called `is_active`. The
# purpose of this flag is to tell us whether the user has been banned
# or deactivated. This will almost never be the case, but
# it is worth checking. Raise an exception in this case.
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
)
# The `validate` method should return a dictionary of validated data.
# This is the data that is passed to the `create` and `update` methods
# that we will see later on.
return {
'email': user.email,
'username': user.username,
'token': user.token
}
views.py
class AuthLogin(APIView):
''' Manual implementation of login method '''
permission_classes = (AllowAny,)
serializer_class = LoginSerializer
def post(self, request, *args, **kwargs):
data = request.data
serializer = LoginSerializer(data=data)
if serializer.is_valid(raise_exception=True):
new_data = serializer.data
return Response(new_data)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)

You can set error_messages attribute for fields you want to override message. In your case:
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255, required=True, error_messages={"required": "Email field may not be blank."})
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True, required=True, error_messages={"required": "Password field may not be blank."})
token = serializers.CharField(max_length=255, read_only=True)
For ModelSerializers you can do this using extra_kwargs property in Meta class.
class SomeModelSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('email', 'password')
extra_kwargs = {
'password': {"error_messages": {"required": "Password field may not be blank."}},
'email': {"error_messages": {"required": "Email field may not be blank."}},
}

you need field-level-validation, try it:
def validate_email(self, value):
# ^^^^^^
if not value:
raise serializers.ValidationError(
'An email address is required to log in.'
)
return value

Related

I want to assign value of logged in user's user_company to the user_company field of the newly created user

When a user creates a user record for a client, the new client should should have the current logged in user's User.user_company value.
In the problem here, I want to assign the value of the logged in user's User.user_company into the new user's clientuser.user_company when save() is called in the view.
here is the serializer below with the clientuser object.
class ClientSerializers(serializers.ModelSerializer):
password2 = serializers.CharField(style={'input_type': 'password'}, write_only=True)
client_name = serializers.CharField(style={'input_type' : 'text'}, required=True)
class Meta:
model = User
fields = ['email', 'username', 'password', 'password2', 'user_type', 'client_name']
extra_kwargs = {
'password': {'write_only': True}, #dont want anyone to see the password
'user_type': {'read_only': True},
}
def save(self):
clientuser = User(
#creating a user record. it will record company fk
email=self.validated_data['email'],
username=self.validated_data['username'],
user_type = 3,
first_name = self.validated_data['client_name'])
#validating the password
password = self.validated_data['password']
password2 = self.validated_data['password2']
if password != password2: #trying to match passwords.
raise serializers.ValidationError({'password': 'Passwords must match.'})
clientuser.set_password(password) #setting the password
clientuser.save() #saving the user
return clientuser
I've tried using
cur_usr = User()
param = 'user_company'
usr_comp = getattr(u, param)
print(f'usr_comp is {usr_comp})
print statement prints usr_comp is None in the terminal
I've also tried using
curr_User_Company = User.user_company
user.user_company = curr_User_Company
it returns the following line in the terminal
raise ValueError(
ValueError: Cannot assign "<django.db.models.fields.related_descriptors.ForwardManyToOneDescriptor object at 0x04B05F28>": "User.user_company" must be a "Company" instance.
Here is my user model
class User(AbstractUser):
email = models.EmailField(verbose_name="email", max_length=60, unique=True)
user_type_data = ((1,"sysAdmin"),(2,"CompanyAdmin"), (3,"Client"), (4,"Employee"))
user_type = models.IntegerField(choices=user_type_data, default=2)
user_company = models.ForeignKey('Company', on_delete=models.CASCADE, null=True, blank=True)
#if user is CompAdmin then company is the company he belongs to
#if user is Client then company is the company he is serviced by
#if user is Employee then company is the company he works for
#if user is sysAdmin then company is null
Here is my view
#csrf_exempt
#permission_classes([IsAuthenticated])
def ClientApi(request):
if request.method == 'POST':
data = JSONParser().parse(request)
serializer = ClientSerializers(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
I'm not sure if this is necessary, but if it is, here is a sample of the data i tried to pass through Postman
{
"email":"Maximillian#amgracing.com",
"username":"Maximillian",
"password":"AmgRacingBetterThanRedBull",
"password2":"AmgRacingBetterThanRedBull",
"client_name" : "MaximillianRacing"
}
Maybe you should used this:
from django.contrib.auth import get_user
and this:
clientuser = User(
#creating a user record. it will record company fk
email=self.validated_data['email'],
username=self.validated_data['username'],
///
user_company = get_user(self.request).user_company /// this line get you user_company from logged user
Finally figured it out. I added current_user parameter to the save() function and assigned the user_company to the value of current_user.user_company
def save(self, current_user):
usr_comp = current_user.user_company
clientUser = User(///
user_company = usr_comp)
In the ClientApi view, here's what I did
serializer.save(current_user = request.user)

How does Django rest framework verify the secondary password?

I'm not using auth, I've added a re_password field to my serializer, I think it only does a consistency check with the password field when a POST request comes in.
But the problem is that if re_password and password are write_only, then PUT and PATCH requests must also pass in these 2 fields.
I guess the consistency validation of re_password and password is reasonable for user registration, but it is not so necessary for updating user information.
What can I do to make re_password and password only required for POST requests?
POST: i need password and re_password field register new user account
PUT/PATCH: i don't need password and re_password as they are not suitable for updating user info
class UserSerializer(serializers.ModelSerializer):
re_password = serializers.CharField(write_only=True, min_length=6, max_length=20, error_messages={
"min_length": "Password at least 6 digits",
"max_length": "Password up to 20 characters",
})
class Meta:
exclude = ("is_delete",)
model = models.User
extra_kwargs = {**CommonSerializer.extra_kwargs, **{
"password": {
"write_only": True,
"min_length": 6,
"max_length": 20,
"error_messages": {
"min_length": "Password at least 6 digits",
"max_length": "Password up to 20 characters",
}
},
}}
def validate_password(self, data):
return hashlib.md5(data.encode("utf-8")).hexdigest()
def validate_re_password(self, data):
return hashlib.md5(data.encode("utf-8")).hexdigest()
def validate(self, validate_data):
if validate_data['password'] != validate_data.pop('re_password'):
raise exceptions.AuthenticationFailed("password not match")
return validate_data
def create(self, validate_data):
instance = models.User.objects.create(**validate_data)
return instance
def update(self, instance, validate_data):
password = validate_data.get("password")
validate_data["password"] = hashlib.md5(
password.encode("utf-8")).hexdigest()
for key, val in validate_data.items():
setattr(instance, key, val)
instance.save()
return instance

How to return a custom, non model value with serializer in Django?

I'd like to return a custom value from my serializer. I'm getting authentication tokens for the user and would like to return them as part of the registration process. At the moment I'm returning the user object and I don't know how to add the tokens into the return object as the tokens are not a part of my User model. Here's the serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ['password', 'is_active', 'staff', 'admin', 'last_login']
def create(self, validated_data):
userName= validated_data['userName']
id= validated_data['id']
dateOfBirth = validated_data['dateOfBirth']
gender = validated_data['gender']
height = validated_data['height']
weight = validated_data['weight']
user = User(userName=userName, id=id, dateOfBirth=dateOfBirth, gender=gender, height=height, weight=weight)
user.set_password("")
user.save()
try:
jwt_token = get_tokens_for_user(user)
except User.DoesNotExist:
raise serializers.ValidationError(
'User does not exists'
)
return user
userName, id, dateOfBirth, height and weight are fields in my User Model. Now I'd like to return also the jwt_token from the create -method to be used in my views.py, is it possible to add it to the user object without editing the Model?
You can return a tuple including the user and the jwt_token like this:
return (user, jwt_token)
and something like this in the views.py:
instance, token = serializer.save()
But if you are using any predefined Veiwsets or Mixins be careful when you pass them this serializer.
Hope this helped.
You can try something like this:
try:
jwt_token = get_tokens_for_user(user)
except User.DoesNotExist:
raise serializers.ValidationError(
'User does not exists'
)
created_data = {
'token_access': jwt_token, # note that jwt_token is dict
'user': UserSerializer(user).data,
}
return created_data
But honestly I recommend you not to mix user and token logic and move this piece into view section:
try:
jwt_token = get_tokens_for_user(user)
except User.DoesNotExist:
raise serializers.ValidationError(
'User does not exists'
)
And then write token and user in Response

Django rest framework how to change: "This field may not be blank." error message

When I am trying to create a new user model, I am getting the following validation error response:
HTTP 400 Bad Request
Allow: POST, OPTIONS
Content-Type: application/JSON
Vary: Accept
{
"phone": [
"This field may not be blank."
],
"name": [
"This field may not be blank."
]
}
What I want to do is how to change the error message to a custom one.
Here is my user serializer code:
from rest_framework import serializers
from django.contrib.auth import get_user_model
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = '__all__'
read_only_fields = ('last_login',)
I already override the validate method but that didn't work neither using this solution:
extra_kwargs = {"phone": {"error_messages": {"required": "A valid phone number is required"}}}
views.py:
class UserList(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
(Custom user) User.py:
class User(AbstractBaseUser):
"""
Custom user model based on phone number as the only identifier
"""
phone = models.CharField(max_length=10, unique=True)
name = models.CharField(max_length=50, blank=False)
password = None
USERNAME_FIELD = 'phone'
objects = UserManager()
def __str__(self):
return self.name
User manager.py:
class UserManager(BaseUserManager):
"""
Custom user model manager where the phone is the unique identifier
"""
def create_user(self, phone, name, **extra_fields):
"""
Create and save a User using the given phone number and country
"""
if not phone:
raise ValueError(_('A valid phone must be provided.'))
if not name:
raise ValueError(_('Your name is required'))
user = self.model(phone=phone, name=name, **extra_fields)
user.set_unusable_password()
user.save(using=self._db)
def create_superuser(self, phone, name, **extra_fields):
"""
Create and save a SuperUser with the given phone and country.
"""
extra_fields.setdefault('is_verified', True)
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError(_('Superuser must have is_staff=True.'))
if extra_fields.get('is_superuser') is not True:
raise ValueError(_('Superuser must have is_superuser=True.'))
return self.create_user(phone, name, **extra_fields)
I know it's a silly problem but I couldn't solve it.
Thanks for your appreciated help.
blank is the key for the error messages dict that is applicable for blank fields so:
extra_kwargs = {'phone': {'error_messages': {'blank': 'New blank error message'}}}
You can validate empty fields in your views.py file
def post(self, request):
phone = request.data.get('phone', False)
name = request.data.get('name', False)
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({
'status': True,
'message': 'User Register Successfully',
'data': serializer.data,
}, status = status.HTTP_201_CREATED)
elif phone == '':
return Response({
'status': False,
'message': 'phone field is not defined!',
}, status = status.HTTP_400_BAD_REQUEST)
elif name == '':
return Response({
'status': False,
'message': 'name field is not defined!',
}, status = status.HTTP_400_BAD_REQUEST)
else :
return Response({
'status': False,
'message': 'Error! Something went wrong!',
}, status = status.HTTP_400_BAD_REQUEST)

How should I handle Django Rest Framework Unique Field Hyperlinked relationships?

I'm trying to write a custom update for DRF HyperlinkRelatedModel Serializer. But really I'm just banging my head against a wall.
It throws up unique constraint errors. First I wanted to have a unique constraint on the email, that wasn't working so I removed it. Now I get the same error on the uuid field.
Can someone please walk me through this, and offer some advice on handling these sorts of relationships.
Below is what I have so far, it's meant to create or update a Recipient and add it to the Email.
I believe I need to write some form of custom validation, I'm not sure how to go about that. Any help will be greatly appreciated.
{
"recipients": [
{
"uuid": [
"recipient with this uuid already exists."
]
}
]
}
Update
This removes the validation error. Now I don't know how to add the validation back in for regular updates.
extra_kwargs = {
'uuid': {
'validators': [],
}
}
Models
class Recipient(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=255)
email_address = models.EmailField()
class Email(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
subject = models.CharField(max_length=500)
body = models.TextField()
recipients = models.ManyToManyField(Recipient, related_name='email')
Serializers
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from schedule_email.models import Recipient, Email, ScheduledMail
class RecipientSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Recipient
fields = ('url', 'uuid', 'name', 'email_address', 'recipient_type')
# I saw somewhere that this might remove the validation.
extra_kwargs = {
'uuid': {
'validators': [],
}
}
class EmailSerializer(serializers.HyperlinkedModelSerializer):
recipients = RecipientSerializer(many=True, required=False)
class Meta:
model = Email
fields = ('url', 'uuid', 'subject', 'body', 'recipients', 'delivery_service')
def create(self, validated_data):
recipient_data = validated_data.pop('recipients')
email = Email.objects.create(**validated_data)
for recipient in recipient_data:
email.recipients.add(Recipient.objects.create(**recipient))
return email
def update(self, instance, validated_data):
recipients_data = validated_data.pop('recipients')
for field, value in validated_data.items():
setattr(instance, field, value)
for recipient_data in recipients_data:
if 'uuid' in recipient_data.keys() and instance.recipients.get(pk=recipient_data['uuid']):
Recipient.objects.update(**recipient_data)
elif 'uuid' in recipient_data.keys() and Recipient.objects.get(pk=recipient_data['uuid']):
instance.recipients.add(Recipient.objects.update(**recipient_data))
elif 'uuid' in recipient_data.keys():
raise ValidationError('No recipient with this UUID was found: %s' % recipient_data['uuid'])
else:
recipient = Recipient.objects.create(**recipient_data)
instance.recipients.add(recipient)
return instance
Below is an example of a post/put request I might make. I probably don't need the uuid field I couldn't workout how to get the Recipient instance from the hyperlink url.
Example Post/Put
{
"subject": "Greeting",
"body": "Hello All",
"recipients": [
{
"url": "http://localhost:8000/api/recipient/53614a41-7155-4d8b-adb1-66ccec60bc87/",
"uuid": "53614a41-7155-4d8b-adb1-66ccec60bc87"
"name": "Jane",
"email_address": "jane#example.com",
},
{
"name": "John",
"email_address": "john#example.com",
}
],
}
With your relation structure, while creating an Email instance, you also pass data for Recipient instances, either new recipients or existing recipients. The validation error you mentioned happens because when you use nested serializers, while creating or updating, DRF calls nested serializer's is_valid method, and when you pass a Recipient data for an existing recipient, DRF tries to validate this as if creating a new Recipient with the data you provided (including uuid), and raises a validation error. To overcome this, in your EmailSerializer, you can disable default validation for recipients field, and add a custom validator method for it, and run the validation like this:
class EmailSerializer(serializers.HyperlinkedModelSerializer):
...
def validate_recipients(self, value):
for recipient_data in value:
if recipient_data.get('uuid'):
try:
recipient = Recipient.objects.get(uuid=recipient_data.get('uuid'))
except Recipient.DoesNotExist:
# raise a validation error here
else:
serializer = RecipientSerializer(recipient)
serializer.is_valid(raise_exception=True) # This will run validation for Recipient update
else:
serializer = RecipientSerializer(data=recipient_data)
serializer.is_valid(raise_exception=True) # This will run validation for Recipient create
return value
The above code first checks if uuid provided for a Recipient, if so, expects it to be the data for an existing Recipient, if not, expects it to be the data for a new recipient, and runs the validations accordingly. Then, in your create method of EmailSerializer, you can create / update the recipients through its serializer like this:
for recipient in recipient_data:
if recipient.get('uuid'):
serializer = RecipientSerializer(Recipient.objects.get(uuid=recipient.get(
'uuid'))) # We know this wont raise an exception because we checked for this in validation
else:
serializer = RecipientSerializer(data=recipient)
serializer.is_valid() # Need to call this before save, even though we know the the data is valid at this point
serializer.save() # This will either update an existing recipient or createa new one
email.recipients.add(serializer.instance)
The approach in update method of the EmailSerilaizer should be similar, but you also take into account cases where a recipient is removed from the list of recipients of an email.
Note: do not raise ValidationError inside create / update methods of serializers, run the validations in validate methods, and use create / update methods only for creating / updating. Write those methods with this mindset: If I made it through to this method, the provided data must be valid, so I will just go on with creating / updating the instance. And write your validations keeping this in mind, too.
Example Serializers
class RecipientSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Recipient
fields = ('url', 'uuid', 'name', 'email_address', 'recipient_type')
extra_kwargs = {
'uuid': {
'validators': [],
}
}
class EmailSerializer(serializers.HyperlinkedModelSerializer):
recipients = RecipientSerializer(many=True, required=False)
class Meta:
model = Email
fields = ('url', 'uuid', 'subject', 'body', 'recipients', 'delivery_service')
def create(self, validated_data):
recipient_data = validated_data.pop('recipients')
email = Email.objects.create(**validated_data)
self.add_recipients(email, recipient_data)
return email
def update(self, instance, validated_data):
recipient_data = validated_data.pop('recipients')
for field, value in validated_data.items():
setattr(instance, field, value)
self.add_recipients(instance, recipient_data)
return instance
def validate_recipients(self, recipients_data):
validated_data = []
for recipient_data in recipients_data:
if recipient_data.get('uuid'):
try:
recipient = Recipient.objects.get(uuid=recipient_data.get('uuid'))
except Recipient.DoesNotExist:
raise ValidationError('No recipient with this UUID was found: %s' % recipient_data.get('uuid'))
serializer = RecipientSerializer(recipient, data=recipient_data)
else:
serializer = RecipientSerializer(data=recipient_data)
serializer.is_valid(raise_exception=True)
validated_data.append(serializer.validated_data)
return validated_data
def add_recipients(self, email, recipients_data):
for recipient_data in recipients_data:
if recipient_data.get('uuid'):
serializer = RecipientSerializer(
Recipient.objects.get(uuid=recipient_data.get('uuid')),
data=recipient_data
)
else:
serializer = RecipientSerializer(data=recipient_data)
serializer.is_valid()
serializer.save()
email.recipients.add(serializer.instance)

Categories

Resources