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)
Related
I have created a CRUD application in Django.
and want to know how I can check if the input JSON data is empty or not
for example:
I am getting a JSON input dictionary as
{'status':'abc','name':'xyz','address':'abc#123','city':'abc'}etc
I want to check if name is not empty and city is not empty.
I don't want a required field in model but to handle the JSON data.
If the JSON input is not valid i.e if username and email is valid then save the data into database else give a warning 400.
serializers.py
from rest_framework import serializers
from .models import Location
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields= '__all__'
views.py
#api_view(['POST'])
def locationAdd(request):
serializer = LocationSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response("data entered successfully")
else:
return Response("data entered is not valid")
return Response(serializer.data)
models.py
class Location(models.Model):
status = models.CharField(max_length=100)
name = models.CharField(max_length=100)
address = models.CharField(max_length=100)
city = models.CharField(max_length=100)
postalCode = models.IntegerField(null=True)
state = models.CharField(max_length=100)
The most appropriate way would be to add extra_kwrgs in the Meta class of your serilalizer.
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ['name', 'city']
extra_kwrgs = {
'name' : {
'required' : True
},
'city' : {
'required' : True
}
}
If you want to implement some other type of validation or manipulation to the particular field then you can use def validate_<field_name> in your serializer.
Here is how it can be implemented -
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ['name', 'city']
extra_kwrgs = {
'name' : {
'required' : True
},
'city' : {
'required' : True
}
}
def validate_name(self, name):
"""
This method is going to verify if the `name` string contains of all white spaces.
"""
if name.isspace():
raise serializer.ValidationError('The name field contain only spaces and no characters.')
return name
You will also have to modify your view to make it work with your serializer's configuration.
#api_view(['POST'])
def locationAdd(request):
serializer = LocationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("data entered successfully")
In your views this line serializer.is_valid(raise_exception=True) will terminate the execution and will raise the validation errors if the provided data is not correct.
For fields that you want to make required even though they are not in your model you can define a custom field on the serializer that is required to override the generated fields
class LocationSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=True)
city = serializers.CharField(required=True)
class Meta:
model = Location
fields = '__all__'
You can do that in the views.py file. Just check like below before calling the serializer
#api_view(['POST'])
def locationAdd(request):
if len(request.data.get('username')) == 0 and len(request.data.get('email')) == 0:
return Response(status=400)
serializer = LocationSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response("data entered successfully")
else:
return Response("data entered is not valid")
return Response(serializer.data)
You can add any validations on your request data like this.
As much as validation in the api backend is reasonable and should always be there for defensive measures, I would also consider doing a check for an empty form on the frontend. That way you dont send unecessary http requests to the backend.
I have the following problem, I have the default User model and Profile model. I want to merge them into one serializer but without nesting - It's just ugly. Instead, I want to have all their fields on the first level. So I created the following (for simplicity profile contains just one Bool field and one relation field):
class UserSerializer(serializers.ModelSerializer):
achievements = serializers.PrimaryKeyRelated(many=True, queryset=Achievements.objects.all())
trusted = serializers.BooleanField()
def create(self, validated_data):
user=User.objects.create_user(
password = validated_data['password'],
username = validated_data['username'],
email = validated_data['email'],
)
Profile.objects.update_or_create(user, defaults={
'trusted': validated_data['trusted'],
'achievements': validatd_data['achievements'],
}
)
return user
class Meta:
model = User
fields = ("id", "username", "email", "password", "trusted", "achievements"),
read_only = ("id",)
extra_kwargs = {
'password': {
'write_only': True,
},
}
Profile is connected to a user via a user field containing models.OneToOneField instance.
When I try to list all profiles I get Error that I need to specify source but I have no idea how and documentation mentions only something that dot notation should be used.
Thanks.
source is argument of serializer field. You should do something like this:
class UserSerializer(serializers.ModelSerializer):
achievements = serializers.PrimaryKeyRelated(many=True, queryset=Achievements.objects.all(), source='profile.achievements')
trusted = serializers.BooleanField(source='profile.trusted')
def create(self, validated_data):
user=User.objects.create_user(
password = validated_data['password'],
username = validated_data['username'],
email = validated_data['email'],
)
Profile.objects.update_or_create(user, defaults={
'trusted': validated_data['trusted'],
'achievements': validatd_data['achievements'],
}
)
return user
I'm using Django rest framework. I want to create admin endpoint where the user can add the member with project permission. following is the data coming from the user.
{
"email" : "name#yopmail.com",
"first_name" : "asd",
"last_name" : "asd",
"projects":[{
"project_id" : 1,
"project_role" : 1 },
{ "project_id" : 1,
"project_role" : 1
}],
"position" : "something something"
}
following is serializer I created to validate and save the data.
class ProjectPermissionSerializer(serializers.Serializer):
"""
Serialiser to hold permissions pair of project_id and role
"""
project_id = serializers.IntegerField(allow_null=False)
project_role = serializers.ChoiceField(PROJECT_ROLES)
def create(self, validated_data):
print(validated_data)
def update(self, instance, validated_data):
pass
class ProjectMemberSerializer(serializers.ModelSerializer):
"""
serializer to add new member with projects
"""
projects = ProjectPermissionSerializer(many=True)
email = serializers.EmailField()
class Meta:
model = User
fields = ('first_name', 'last_name', 'email', 'position', 'projects')
def create(self, validated_data):
permission_data = validated_data.pop('projects')
emailstatus = {"email_id": validated_data.pop('email')}
emailobj, created = EmailStatus.objects.get_or_create(**emailstatus)
validated_data['email'] = emailobj
project_member = User.objects.create(**validated_data)
return project_member
still, after popping the projects from validated_data, I'm getting following error.
AttributeError: Got AttributeError when attempting to get a value for field `projects` on serializer `ProjectMemberSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `User` instance.
Original exception text was: 'User' object has no attribute 'projects'.
Looks like Your model dont have field named projects, in this case you need to specify source of the field, to link serializer's field with model's field e.g. 'project_set':
projects = ProjectPermissionSerializer(many=True, source='project_set')
I've 2 models:-
class Users(models.Model):
first_name = models.CharField(max_length=255)
middle_name = models.CharField(max_length=255)
class UserAddress(models.Model):
line1 = models.CharField(max_length=255)
country = models.CharField(max_length=255)
user = models.ForeignKey(Users)
The user model & user address model. Following are the 2 serializers.
class UserAddressSerializer(ModelSerializer):
class Meta:
model = UserAddress
exclude = ('id', 'user')
class UserSerializer(ModelSerializer):
address = UserAddressSerializer(many=True)
class Meta:
model = Users
fields = '__all__'
def create(self, validated_data):
address = validated_data.pop('address', [])
user = Users.objects.create(**validated_data)
for ad in address:
UserAddress.objects.create(user=user, **ad)
return user
The data I receive from the client is
{
"first_name": "string",
"last_name": "string",
"address": [{
"line1": "asd",
"country": "asd",
}],
}
This is how I create a new user and its corresponding address.
class UserCreate(GenericAPIView):
serializer_class = UserSerializer
def post(self, request, *args, **kwargs):
data = request.data
serializer = UserSerializer(data=data)
if not serializer.is_valid():
return
user = serializer.save()
response = {
'user_id': user.uuid
}
return
Now, upon getting the user details back, I receive an error saying
AttributeError: Got AttributeError when attempting to get a value for field `address` on serializer `UserSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Users` instance.
Original exception text was: 'Users' object has no attribute 'address'.
This is how I get the details of the user, including the address.
class UserDetails(GenericAPIView):
queryset = Users.objects.all()
serializer_class = UserSerializer
lookup_field = 'uuid'
def get(self, request, uuid, *args, **kwargs):
user = Users.get_user(uuid)
if not user:
return
serializer = UserSerializer(instance=user)
return
I'd read this example of nested relationship, and am doing exactly the same way. why is the error coming up?
Also, can this code be shorten up more (in a nicer clean way) using some DRF mixins? If yes, then how?
I think the most simple solution for your case is: in model UserAddress add related_name='address'
class UserAddress(models.Model):
line1 = models.CharField(max_length=255)
country = models.CharField(max_length=255)
user = models.ForeignKey(Users, related_name='address')
# ^^^^^^^^^^^^^^^
or you can add sourse property in serializer:
class UserSerializer(ModelSerializer):
address = UserAddressSerializer(source='useraddress_set', many=True)
Serializer try to find attribute 'address' in the model User, but by default it is modelname underscore set (useraddress_set in your case), and you try other name, so you can set in the model or specify by source.
in the example you can look on models and find the related_name='tracks'
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