Error after overriding create method in serializer - django

I'm Overriding create method of serializer in order to manipulate validated_data and create object in a model, Although it works, in the end I get below error, i am not able to figure out why after lot of research.
AttributeError: Got AttributeError when attempting to get a value for field `shift_time` on serializer `PunchRawDataAndroidSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `PunchRawData` instance.
Original exception text was: 'PunchRawData' object has no attribute 'shift_time'.
class PunchRawDataAndroidSerializer(serializers.ModelSerializer):
employee_id = serializers.CharField()
shift_id = serializers.CharField()
work_location_id = serializers.CharField()
shift_time = serializers.TimeField()
class Meta:
model = PunchRawData
fields = ['employee_id', 'shift_id','work_location_id', 'punch_type', 'actual_clock_datetime',
'emp_photo', 'created_at', 'updated_at','shift_time']
def create(self, validated_data):
validated_data.pop('shift_time')
request_data = self.context.get('request')
user = request_data.user
validated_data['user'] = user
data = validated_data
return PunchRawData.objects.create(**data)
class PunchRawDataAndroidViewSet(viewsets.ModelViewSet):
serializer_class = PunchRawDataAndroidSerializer
parser_classes = (MultiPartParser, FileUploadParser)
edit:
class PunchRawData(models.Model):
PUNCH_TYPES = [("in", "Punch IN"), ("out", "Punch Out")]
employee = models.ForeignKey(Employee, related_name="punch_employee", on_delete=models.CASCADE)
shift = models.ForeignKey(WorkShift, on_delete=models.CASCADE)
work_location = models.ForeignKey(HRMLocation, blank=True, on_delete=models.CASCADE,
null=True, related_name="punch_work_location")
punch_type = models.CharField(max_length=255, null=True, blank=True, choices=PUNCH_TYPES)
user = models.ForeignKey("useraccounts.User", on_delete=models.CASCADE)
actual_clock_datetime = models.DateTimeField(null=True, blank=True)
emp_photo = models.ImageField(upload_to="selfies/%Y/%m/%d/%I/%M/%S/")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
strl = "{emp_id} [{shift_id}]".format(emp_id=self.employee.emp_id,
shift_id=self.shift.shift_id)
return strl
class Meta:
verbose_name = "Punch Raw Data"
verbose_name_plural = "Punch Raw Data"
I get shift_time from frontend and it is not from model, hence i'm poping it out from validated_data in create method. is error related to modelviewset?

Your model doesn't have the shift_time attribute. So if you try to save it, you will end with
PunchRawData() got an unexpected keyword argument 'shift_time'
At the other hand you are getting AttributeError, because serializers.to_representation() tries to get a non-existing attribute when showing your freshly saved object.
If this should be a read-only attribute, you may do the following:
shift_time = serializers.TimeField(read_only=True)
and than remove the
validated_data.pop('shift_time')
from PunchRawDataAndroidSerializer.create(). You don't need this any more, because it is never submitted from your client.
If you need the opposite – your client should provide you that field, but you don't want it saved in your model, than the only thing, you should do, is:
shift_time = serializers.TimeField(write_only=True)
And if you need it to be bidirectional, than you should add it to your model.
Hope this helps.

Adding to #wankata's answer we can override __init__ method to have write_only field for only create method.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context['view'].action == 'create':
self.fields['shift_time'].write_only = True

Generic viewsets of django-rest-framework return the serialized representation of the model in response, so it's try to serialize the model including the shift_time key.
To avoid this problem you can specify the shift_time field as write_only. documentation
modify the Meta class on your model
class Meta:
model = PunchRawData
fields = ['employee_id', 'shift_id','work_location_id', 'punch_type', 'actual_clock_datetime',
'emp_photo', 'created_at', 'updated_at','shift_time']
extra_kwargs = {'shift_time': {'write_only': True}}

Related

Polymorphic models serializer

I'm using a Polymorphic model for setting up notifications:
My models:
class Notification(PolymorphicModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_by = models.ForeignKey(ElsUser, on_delete=models.CASCADE, default=None, related_name="creatednotifications")
created_on = models.DateTimeField(default=timezone.now)
created_for = models.ForeignKey(ElsUser, on_delete=models.CASCADE, default=None, related_name="receivednotifications")
read = models.DateTimeField(default=None, null=True, blank=True)
message = models.CharField(default=None, blank=True, null=True, max_length=800)
#property
def total(self):
return self.objects.filter(created_for=self.request.user).count()
#property
def unread(self):
return self.objects.filter(created_for=self.request.user,read=None).count()
#property
def read(self):
return self.objects.filter(created_for=self.request.user).exclude(read=None).count()
class WorkflowNotification(Notification):
# permission_transition = models.ForeignKey(WorkflowStatePermissionTransition, on_delete=models.CASCADE)
action = models.ForeignKey(UserAction, on_delete=models.CASCADE)
Currently i have just one model WorkFlowNotification inheriting from the Polymorphic model,but many would be there in the future.
Im trying to get the count(total) of notifications for the logged in user in the API ..total is given as property field to help in the same
my serializer:
class NotificationSerializer(serializers.ModelSerializer):
total = serializers.ReadOnlyField()
read = serializers.ReadOnlyField()
unread = serializers.ReadOnlyField()
class Meta:
model = Notification
fields = ['id', 'total','read', 'unread']
In the view:
class NotificationsMeta(generics.ListAPIView):
serializer_class = NotificationSerializer
queryset = Notification.objects.all()
When i try to run the server it shows:
Got AttributeError when attempting to get a value for field `total` on serializer `NotificationSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `WorkflowNotification` instance.
Original exception text was: Manager isn't accessible via WorkflowNotification instances.
Since you need the 'meta data' only, what is the use of making a model serializer? Or any serializer, for that matter? Serializers will give you serialized instances of the objects of your model. So if you have multiple objects, you will get multiple serialized objects in response.
Just make your view a normal APIView. Since there is no necessity of serializing anything.
class NotificationsMeta(APIView):
def get(self, request, format=None):
qs = Notification.objects.filter(created_for=self.request.user)
response = {
'total': qs.count(),
'read': qs.filter(read=None).count(),
'unread': qs.exclude(read=None).count()
}
return Response(response)
Now remove those property functions from your model.
I didn't test your queries, just copied them from your model. You will need to check if they are working properly. Hope this helps.
I am not sure about how calling a model property who is responsible for querying in model can give appropriate data from serializer. Unfortunately i do have knowledge gap about that. I am thinking about an alternative solution. I hope following should work.
class NotificationSerializer(serializers.ModelSerializer):
total = serializers.serializers.SerializerMethodField()
read = serializers.ReadOnlyField()
unread = serializers.ReadOnlyField()
class Meta:
model = Notification
fields = ['read', 'unread']
def get_total(self, obj):
user = self.context['request'].user
return Notification.objects.filter(created_for=user).count()
If this work then you can able to do similar kind of thing for read and unread too.
In order to get notification for current_user we need to overwrite get_queryset from view.
class NotificationsMeta(generics.ListAPIView):
serializer_class = NotificationSerializer
def get_queryset(self):
return Notification.objects.filter(created_for=self.request.user)

Invalid data. Expected a dictionary, but got str error with serializer field in Django Rest Framework

I'm using Django 2.x and Django REST Framework.
I have two models like
class Contact(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.PROTECT)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100, blank=True, null=True)
modified = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
class AmountGiven(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
given_date = models.DateField(default=timezone.now)
created = models.DateTimeField(auto_now=True)
the serializer.py the file has serializers defined as
class ContactSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contact
fields = ('id', 'first_name', 'last_name', 'created', 'modified')
class AmountGivenSerializer(serializers.ModelSerializer):
contact = ContactSerializer()
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'amount', 'given_date', 'created'
)
views.py
class AmountGivenViewSet(viewsets.ModelViewSet):
serializer_class = AmountGivenSerializer
def perform_create(self, serializer):
save_data = {}
contact_pk = self.request.data.get('contact', None)
if not contact_pk:
raise ValidationError({'contact': ['Contact is required']})
contact = Contact.objects.filter(
user=self.request.user,
pk=contact_pk
).first()
if not contact:
raise ValidationError({'contact': ['Contact does not exists']})
save_data['contact'] = contact
serializer.save(**save_data)
But when I add a new record to AmountGiven model and passing contact id in contact field
it is giving error as
{"contact":{"non_field_errors":["Invalid data. Expected a dictionary, but got str."]}}
When I remove contact = ContactSerializer() from AmountGivenSerializer, it works fine as expected but then in response as depth is set to 1, the contact data contains only model fields and not other property fields defined.
I'm not a big fan of this request parsing pattern. From what I understand, you want to be able to see all the contact's details when you retrieve an AmountGiven object and at the same time be able to create and update AmountGiven by just providing the contact id.
So you can change your AmountGiven serializer to have 2 fields for the contact model field. Like this:
class AmountGivenSerializer(serializers.ModelSerializer):
contact_detail = ContactSerializer(source='contact', read_only=True)
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
Note that the contact_detail field has a source attribute.
Now the default functionality for create and update should work out of the box (validation and everything).
And when you retrieve an AmountGiven object, you should get all the details for the contact in the contact_detail field.
Update
I missed that you need to check whether the Contact belongs to a user (however, I don't see a user field on your Contact model, maybe you missed posting it). You can simplify that check:
class AmountGivenViewSet(viewsets.ModelViewSet):
serializer_class = AmountGivenSerializer
def perform_create(self, serializer):
contact = serializer.validated_data.get('contact')
if contact.user != self.request.user:
raise ValidationError({'contact': ['Not a valid contact']})
serializer.save()
Override the __init__() method of AmountGivenSerializer as
class AmountGivenSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(AmountGivenSerializer, self).__init__(*args, **kwargs)
if 'view' in self.context and self.context['view'].action != 'create':
self.fields.update({"contact": ContactSerializer()})
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'amount', 'given_date', 'created'
)
Description
The issue was the DRF expects a dict like object from contact field since you are defined a nested serializer. So, I removed the nested relationship dynamically with the help of overriding the __init__() method
For those who got here but have relatively simple serializers, this error can also occur when the request data is malformed, in my case JSON encoded twice.
The serializer will decode the JSON, but as it is encoded twice request.data will still be a string. The error therefore makes sense as a "dictionnary" was expected, but we still have a "string".
You can check the output of the following to confirm whether this is the issue you are experiencing:
print(type(request.data)) # Should be <class 'dict'>

How to set automatically 'many' flag of django serializer depending on input being a list or a single item

I have the following machine model.
class Machine(models.Model):
operators = models.ManyToManyField(User, related_name='machines', blank=True)
elasticsearch_id = models.PositiveIntegerField(default=None, null=True, blank=True)
company = models.ForeignKey(Company, default=None, null=True, blank=True,on_delete=models.SET_DEFAULT)
machine_brand = models.CharField(max_length=200, null=False)
machine_model = models.CharField(max_length=200, default='')
machine_picture = models.URLField(max_length=200, null=True)
tools = models.ManyToManyField('Tool', default=None, blank=True)
clustered_tags = JSONField(null=True)
elasticsearch_tags = JSONField(null=True, blank=True, default=DEFAULT_TAG_MAP)
machine_slug = models.SlugField()
With the following serializer.
class MachineSerializer(serializers.ModelSerializer):
class Meta:
model = Machine
fields = '__all__'
In my views, I am filtering the data on the company the logged in users belongs to. Now, I want to serialize the object and return it to the client. However, I don't know beforehand whether the queryset is a list of objects or a single object so that I can set the many flag of the serializer to true or false.
#api_view(['GET','POST'])
def manage_operators(request):
user_machines = Machine.objects.filter(company=request.user.company)
user_machines_ser = MachineSerializer(user_machines, many=True)
return Response({'machines': user_machines_ser.data})
Is there any elegant way to solve this? I could solve it this way but there must be a better way of doing it.
if len(user_machines) > 0 :
user_machine_ser = MachineSerializer(user_machines, many=True)
else:
user_machine_ser = MachineSerializer(user_machines, many=False)
Any input much appreciated!
Since you are fetching a QuerySet every time, you don't have to set many=False if there is only one item in the QuerySet.
So you can safely use
user_machine_ser = MachineSerializer(user_machines, many=True)
everytime, no matter how many objects are in the QuerySet.
Since you are passing a QuerySet, you can use the count() [Django doc] method in the __init__() method of MachineSerializer by overriding it.
class MachineSerializer(serializers.ModelSerializer):
class Meta:
model = Machine
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if kwargs['instance'].count() > 1: # count() method used here <<<<<
kwargs['many'] = True
else:
kwargs['many'] = False

Related Object serializer Django restframework

My question is somewhat related to this one with some differences. I have a model similar to this one:
class Project(models.Model):
project_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
created_by_id = models.ForeignKey('auth.User', related_name='project', on_delete=models.SET_NULL, blank=True, null=True)
created_by = models.CharField(max_length=255, default="unknown")
created = models.DateTimeField(auto_now_add=True)
With the following serializer:
class ProjectSerializer(serializers.ModelSerializer):
created_by = serializers.ReadOnlyField(source='created_by_id.username')
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created')
And corresponding view:
class projectsView(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(created_by_id=self.request.user)
This code behaves like I want but forces information redundancy and does not leverage the underlying relationnal database. I tried to use the info from the linked question to achieve a "write user id on database but return username on "get"" in a flat json without success:
Removing the "created_by" field in the model. Replacing the serializer with:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Which would NOT 100% give me what I want, i.e. replace the user id with the username in a flat json but return something like: {'project_id': <uuid>, 'created_by': <user json object>, 'created': <data>}. But still I get a {'created_by_id': ['This field is required.']} 400 error.
Question: How can I write a user id to a database object from the request.user information to refer to an actual user id but return a simple username in the GET request on the projectsView endpoint without explicitly storing the username in the Model? Or more generally speaking, how can I serialize database objects (Django models) into customer json response by using default serialization DRF features and default DRF views mixins?
Alternate formulation of the question: How can I store an ID reference to another DB record in my model (that can be accessed without it being supplied by the payload) but deserialize a derived information from that object reference at the serializer level such as one specific field of the referenced object?
I would recommend you to use Two different serializers for Get and POST operations. Change your serializers.py as
class ProjectGetSerializer(serializers.ModelSerializer):
created_by_id = serializers.StringRelatedField()
class Meta:
model = Project
fields = '__all__'
class ProjectCreateSerializer(serializers.ModelSerializer):
created_by_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault())
def create(self, validated_data):
return Project.objects.create(**validated_data, created_by=validated_data['created_by_id'].username)
class Meta:
model = Project
fields = '__all__'
Also, I reccomend ModelViewSet for API class if you are looking for CRUD operations. Hence the view will be like this,
class projectsView(viewsets.ModelViewSet):
queryset = Project.objects.all()
def get_serializer_class(self):
if self.action == 'create':
return ProjectCreateSerializer
return ProjectGetSerializer
So, the payload to create Project is,
{
}
One thing you should remember, while you trying to create Project user must logged-in
UPDATE - 1
serializer.py
class ProjectCreateSerializer(serializers.ModelSerializer):
created_by_id = serializers.StringRelatedField()
class Meta:
model = Project
fields = '__all__'
def create(self, validated_data):
return Project.objects.create(**validated_data, created_by_id=self.context['request'].user)
views.py
class projectsView(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectCreateSerializer
The error is in the write_only field options. The required parameter default value is set to True while the intent is to not make it required if we take a look at the model. Here in the view, I use the perform_create as post processing to save on the Model DB representation. Since required default value is True at the creation level, the first .save() to the DB fails. Since this is purely internal logic, the required is not necessary. So simply adding the required=False option on the PrimaryKeyRelatedField does the job:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True, required=False)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Enforcing the required=True at the Model level as well would require to override the .save function of the serializer if I insist on playing with the logic purely at the serializer level for deserialization. There might be a way to get the user ref within the serializer as well to keep the views implementation even more 'default'... This can be done by using the default value from Jerin:
class ProjectSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by',
write_only=True,
required=False,
default=serializers.CurrentUserDefault())
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
Now to flaten the json with username only, you need to use a slug field instead of the UserSerializer:
class ProjectSerializer(serializers.ModelSerializer):
created_by = serializers.SlugRelatedField(
queryset=User.objects.all(), slug_field="username")
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), source='created_by', write_only=True, required=False)
class Meta:
model = Project
fields = ('project_id', 'created_by', 'created_by_id', 'created')
And then only the username field value of the User Model will show at the create_by json tag on the get payload.
UPDATE - 1
After some more tweaking here is the final version I came up with:
class ProjectSerializer(serializers.ModelSerializer):
created_by_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), write_only=True, required=False, default=serializers.CurrentUserDefault())
created_by = serializers.SerializerMethodField('creator')
def creator(self, obj):
return obj.created_by_id.username
class Meta:
model = Project
fields = ('project_id', 'created_by_id', 'created_by', 'created')

Nested objects save in DRF

Models:
class Owner(models.Model):
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class SomeThing(models.Model):
own_id = models.IntegerField(unique=True)
description = models.CharField(max_length=255, blank=True)
owner = models.ForeignKey(Owner, blank=True, null=True)
def __unicode__(self):
return self.description
Serializers:
class OwnerNameField(serializers.RelatedField):
def to_internal_value(self, data):
pass
def to_representation(self, value):
return value.name
def get_queryset(self):
queryset = self.queryset
if isinstance(queryset, (QuerySet, Manager)):
queryset = queryset.all()
lista = [Owner(name="------------")]
lista.extend(queryset)
return lista
class OwnerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Owner
fields = ('name', 'id')
class ThingSerializer(serializers.ModelSerializer):
owner = OwnerNameField(queryset=Owner.objects.all())
class Meta:
model = SomeThing
fields = ('own_id', 'description', 'owner')
Basically it works as intended. But when i add some fields to Owner class i would like to see all these fields in output of ThingSerializer (and be able to parse them - string doesn't suit here). I could change field owner to owner = OwnerSerializer() which gives me what i need. But when i want to add SomeThing object (tested in API browser) i also need add new Owner object - and i don't want it, i want use existing Owner object. How can i achieve it?
Finally i got it. This question describes exactly my problem and provided answers work as a charm!