Django REST Framework create and update nested objects - django

I am trying to create or update a nested object if the object exists, I tried to use the create_or_update method, now the create works fine, but the update failed and said the pk already existed.
My Model:
class ContentHotel(models.Model):
hotel_id = models.IntegerField(unique=True, blank=True, primary_key=True)
star = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'content_hotels'
ordering = ('hotel_id',)
def __str__(self):
return str(self.hotel_id)
class RateHotel(models.Model):
rate_hotel_id = models.IntegerField(blank=True, unique=True, primary_key=True)
content_hotel = models.ForeignKey(ContentHotel, on_delete=models.CASCADE, related_name='rate_hotels')
source_code = models.CharField(max_length=20, blank=True, null=True)
class Meta:
managed = False
db_table = 'rate_hotels'
ordering = ('rate_hotel_id',)
def __str__(self):
return str(self.rate_hotel_id)
My Serializers:
# To handle RateHotel object operations
class RateHotelSerializer(serializers.ModelSerializer):
class Meta:
model = RateHotel
fields = __all__
# To handle nested object operations
class RateHotelSerializerTwo(serializers.ModelSerializer):
class Meta:
model = RateHotel
fields = __all__
read_only_fields = ('content_hotel',)
class ContentHotelSerializer(serializers.ModelSerializer):
rate_hotels = RateHotelSerializerTwo(many=True)
class Meta:
model = ContentHotel
fields = __all__
def create(self, validated_data):
rate_hotels_data = validated_data.pop('rate_hotels')
hotel_id = validated_data.pop('hotel_id')
content_hotel, created = ContentHotel.objects.update_or_create(hotel_id=hotel_id, defaults={**validated_data})
for rate_hotel_data in rate_hotels_data:
rate_hotel_id = rate_hotel_data.pop('rate_hotel_id')
RateHotel.objects.update_or_create(rate_hotel_id=rate_hotel_id, content_hotel=content_hotel,
defaults=rate_hotel_data)
return content_hotel
def update(self, instance, validated_data):
rate_hotels_data = validated_data.pop('rate_hotels')
rate_hotels = list(instance.rate_hotels.all())
for key in validated_data:
instance.key = validated_data.get(key, instance.key)
instance.save()
for rate_hotel_data in rate_hotels_data:
rate_hotel = rate_hotels.pop(0)
for key in rate_hotel_data:
rate_hotel.key = rate_hotel_data.get(key, rate_hotel.key)
rate_hotel.save()
return instance
JSON:
# Post Request - Create:
{
"hotel_id": -1,
"star": null,
"rate_hotels": [{"rate_hotel_id": -1}]
}
# Post Response:
{
"hotel_id": -1,
"star": null,
"rate_hotels": [
{
"content_hotel": -1,
"rate_hotel_id": -1,
"source_code": null,
}
]
}
# Post Request - Update:
{
"hotel_id": -1,
"star": 2,
"rate_hotels": [{"rate_hotel_id": -1, "source_code": "-1"}]
}
{
"hotel_id": [
"content hotel with this hotel id already exists."
],
"rate_hotels": [
{
"rate_hotel_id": [
"rate hotel with this rate hotel id already exists."
]
}
],
"status_code": 400
}
If I send a put request to update the nested object, the content hotel part works correctly, but the rate hotel part still said 'rate_hotel_id' already exists.
Can anyone help me fix this issue? I cannot find any related source online, thanks in advance!

Hmmmm, this looks like you could be looking for for "update if exists else create" use case, and Django has a get_or_create() method for performing what you want, see the docs here for reference: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get-or-create
However, for you it could look something like this:
id = 'some identifier that is unique to the model you wish to lookup'
content_hotel, created = RateHotel.objects.get_or_create(
rate_hotel_id=rate_hotel_id,
content_hotel=content_hotel,
defaults=rate_hotel_data
)
if created:
# means you have created a new content hotel, redirect a success here or whatever you want to do on creation!
else:
# content hotel just refers to the existing one, this is where you can update your Model
I would also advise removing the hotel_id and hotel_rate_id as primary keys:
class ContentHotel(models.Model):
hotel_id = models.IntegerField(unique=True, blank=True,)
star = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'content_hotels'
ordering = ('hotel_id',)
def __str__(self):
return str(self.hotel_id)
class RateHotel(models.Model):
rate_hotel_id = models.IntegerField(blank=True, unique=True,)
content_hotel = models.ForeignKey(ContentHotel, on_delete=models.CASCADE, related_name='rate_hotels')
source_code = models.CharField(max_length=20, blank=True, null=True)
class Meta:
managed = False
db_table = 'rate_hotels'
ordering = ('rate_hotel_id',)
def __str__(self):
return str(self.rate_hotel_id)
The update_or_create() method does not accept a primary key, but can accept other unique database fields to perform the update:
So, an update_or_create() would look like so:
obj, created = Person.objects.update_or_create(
first_name='John', last_name='Lennon',
defaults={'first_name': 'Bob'},
)

Your create code looks fine, but there are some bigger issues with your update code which you need to address before we can know where the real issue lies I think.
for key in validated_data:
instance.key = validated_data.get(key, instance.key)
instance.save()
Here you are trying to loop through the values in the validated data and assign the values of that key to that attribute on the model. What you are actually doing is setting the attribute key on the model to the value of each key in validated_data. To better illustrate the issue I'll give you an example which corrects the issue:
for key in validated_data:
setattr(instance, key, validated_data.get(key, getattr(instance, key))
instance.save()
Here rather than trying to set instance.key we use the python built-in method setattr to set the attribute with the name that matches the value of the variable key to the value which we pull from validated_data.
I'm not sure correcting this will solve your issue though. I'd make sure you're correctly sending your request to the correct url with the proper header values to get to the update method. It could be that Django Rest Framework is preventing the creation behind the scenes because it thinks you're sending the request as a create operation and it finds the primary keys already.

Related

django-rest-framework access field inside serializer

So I have a model like this
class DataSheet(BaseModel):
"""
Represents a single dataSheet.
dataSheets have their own model at the core. Model data is added to
the dataSheets in the form of separate records.
"""
class Meta:
verbose_name = 'datasheet'
verbose_name_plural = 'datasheets'
ordering = ['position', 'cluster']
required_db_features = {
'supports_deferrable_unique_constraints',
}
constraints = [
models.UniqueConstraint(
fields=['position', 'cluster'],
name='deferrable_unique_datasheet_position',
deferrable=models.Deferrable.DEFERRED
)
]
def __str__(self):
return self.name
objects = managers.DataSheetsManager()
positions = managers.PositionalManager()
position = models.PositiveSmallIntegerField(db_index=True, editable=True)
name = models.CharField(max_length=100, validators=[MinLengthValidator(2)], db_index=True)
description = models.CharField(max_length=1024, null=True, blank=True, db_index=True)
owner = models.ForeignKey('api_backend.Member', on_delete=models.CASCADE, db_index=True, editable=False)
fields = models.ManyToManyField('api_backend.Field')
overwrites = models.ManyToManyField('api_backend.RoleOverwrite')
parent = models.ForeignKey('api_backend.Category', on_delete=models.CASCADE, null=True, blank=True)
cluster = models.ForeignKey('api_backend.Cluster', on_delete=models.CASCADE, editable=False)
REQUIRED_FIELDS = [name, owner, cluster]
and a serializer like this
class DataSheetSerializer(serializers.ModelSerializer):
"""
A serialized DataSheet Object.
Datasheets have their own:
- array of fields
- array of role-overwrites
"""
def get_fields(self):
fields = super(DataSheetSerializer, self).get_fields()
fields['parent'].queryset = self.cluster.categories.all()
return fields
class Meta:
model = DataSheet
read_only_fields = ['position']
fields = '__all__'
# need to make sure that the parent category of the datasheet
# belongs to the datasheet's cluster only.
fields = partial.PartialFieldSerializer(many=True, read_only=True)
overwrites = partial.PartialOverWriteSerializer(many=True, read_only=True)
the thing is, I want to access the serializer model's cluster field inside of the get_fields method. However, I couldn't do the same. Can someone help me?
I've seen other answers involving initial_data, but that doesn't work here.
fields['parent'].queryset = self.cluster.categories.all()
cluster is an unresolved reference here.
self in get_fields is DataSheetSerializer instance not DataSheet model instance. hence it should not have cluster property. you can not access model DataSheet instance in get_fields as it gets fields from class DataSheet not from its instance. you can validate the field like
class DataSheetSerializer(serializers.ModelSerializer):
# ... other code
def validate(self, data):
parent = data.get('parent')
# check if parent is valid i.e in queryset
# if yes return data
# else raise serializers.validationError

Error after overriding create method in serializer

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}}

Django ModelChoiceField Issue

I've got the following Situation, I have a rather large legacy model (which works nonetheless well) and need one of its fields as a distinct dropdown for one of my forms:
Legacy Table:
class SummaryView(models.Model):
...
Period = models.CharField(db_column='Period', max_length=10, blank=True, null=True)
...
def __str__(self):
return self.Period
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'MC_AUT_SummaryView'
Internal Model:
class BillCycle(models.Model):
...
Name = models.CharField(max_length=100, verbose_name='Name')
Period = models.CharField(max_length=10, null=True, blank=True)
Version = models.FloatField(verbose_name='Version', default=1.0)
Type = models.CharField(max_length=100, verbose_name='Type', choices=billcycle_type_choices)
Association = models.ForeignKey(BillCycleAssociation, on_delete=models.DO_NOTHING)
...
def __str__(self):
return self.Name
Since I don't want to connect them via a Foreign Key (as the SummaryView is not managed by Django) I tried a solution which I already used quite a few times. In my forms I create a ModelChoiceField which points to my Legacy Model:
class BillcycleModelForm(forms.ModelForm):
period_tmp = forms.ModelChoiceField(queryset=SummaryView.objects.values_list('Period', flat=True).distinct(),
required=False, label='Period')
....
class Meta:
model = BillCycle
fields = ['Name', 'Type', 'Association', 'period_tmp']
And in my view I try to over-write the Period Field from my internal Model with users form input:
def billcycle_create(request, template_name='XXX'):
form = BillcycleModelForm(request.POST or None)
data = request.POST.copy()
username = request.user
print("Data:")
print(data)
if form.is_valid():
initial_obj = form.save(commit=False)
initial_obj.ModifiedBy = username
initial_obj.Period = form.cleaned_data['period_tmp']
initial_obj.Status = 'Creating...'
print("initial object:")
print(initial_obj)
form.save()
....
So far so good:
Drop Down is rendered correctly
In my print Statement in the View ("data") I see that the desired infos are there:
'Type': ['Create/Delta'], 'Association': ['CP'], 'period_tmp': ['2019-12']
Still I get a Select a valid choice. That choice is not one of the available choices. Error in the forms. Any ideas??

Django nested serializer allow_null=True

I have a nested serializer and I want to activate the allow_null to true, but it doesn't work.
TOP object have a nested Down object, the related_name must be present in the TOP object but with a null value. If the down object is not null all down object fields are required.
Example request with all fields in down object (this one works fine) :
{
"title": "Titre new rgfdgfdgthtrh",
"downs": {
"type": "Type example",
"is_external": true,
},
}
Example that i tryed to do : request when down object is null (this one doesn't work)
{
"title": "Titre new ",
"downs": {},
}
I have tryed with "downs": None or Null without success.
My views :
# My Views.py
class Top(models.Model):
class Meta:
verbose_name = _('Top')
verbose_name_plural = _('Tops')
top_guid = models.UUIDField(
primary_key=True,
unique=True,
default=uuid.uuid4,
editable=False)
title = models.CharField(
help_text=_('Title'),
verbose_name=_('title'),
max_length=100,
blank=False
)
class Down(models.Model):
top = models.OneToOneField(
Top,
on_delete=models.CASCADE,
help_text=_('Top'),
verbose_name="top",
related_name="downs"
)
type = models.CharField(
help_text=_('Type'),
verbose_name=_('type'),
max_length=30,
blank=False
)
is_external = models.BooleanField(
help_text=_('external (default = false)'),
verbose_name=_('external'),
blank=False,
default=False
)
and my serializers
# My serializers.py
class DownSerializer(serializers.ModelSerializer):
class Meta:
model = Down
fields = '__all__'
class TopSerializer(serializers.ModelSerializer):
downs = DownSerializer(many=False, required=False, allow_null=True)
class Meta:
model = Top
fields = ('top_guid', 'title', 'downs',)
def create(self, validated_data):
"""
Create and return a new `Topic` instance.
"""
downs_data = validated_data.pop('downs')
top = Top.objects.create(**validated_data)
Down.objects.create(top=top, **downs_data)
return top
def update(self, instance, validated_data):
"""
Update and return an existing `Topic` instance.
"""
# get bim_snippet data and bim_snippet object
downs_data = validated_data.pop('downs')
downs = instance.downs
# update top data and save top object
instance.title = validated_data.get('title', instance.title)
instance.top_type = validated_data.get('top_type', instance.top_type)
instance.save()
# update down data and save down object
downs.snippet_type = downs_data.get('type', downs.snippet_type)
downs.is_external = downs_data.get('is_external', downs.is_external)
downs.save()
return instance
Thank's a lot.
I think that if you add arguments like allow_null=True or read_only=False in your serializer class, you need to recreate your sqlite3 database. read_only was not working, but just after recreate the db it works fine. (makemigrations and migrate seem's to be not enought)

field does not allow filtering (no relation used)

I have a problem with django and tastypie
Given is the following code:
class CandidatePollResource(ModelResource):
class Meta:
queryset = Candidate.objects.all()
resource_name = "candidate-poll"
filtering = {"status": ALL }
class Candidate(Profile):
"""
This profile stores all information about a candidate.
"""
status = models.CharField(_('status'), max_length=15, blank=True, choices=CANDIDATE_STATUS_CHOICES)
class Profile(models.Model):
"""
Abstract basic class all profiles should inherit.
"""
user = models.OneToOneField(CustomUser,related_name='%(class)s',)
invitationdate = models.DateField(_('invitationdate'), null=True, blank=True)
confirmationdate = models.DateField(_('confirmationdate'), null=True, blank=True)
activation_key = models.CharField(_('activation_key'), max_length=32, blank=True)
# Adding of "class" here is important for this to work. See
# http://thedjangoforum.com/board/thread/417/reverse-query-name-for-field-clash/?page=1
created_by = models.ForeignKey(CustomUser, related_name='%(class)s_created', blank=True, null=True)
objects = ProfileManager()
class Meta:
abstract = True
Each time we try to do a call to filter the result set ( http://localhost:3000/api/v1/candidate-poll/?status__exact=new ), I always get the following error
The 'status' field does not allow filtering.
How can I enable filtering on that field?
I think that your syntax is not exactly correct.
Instead of:
filtering = {"status": ("exact", "in",), }
try:
filtering = {"status": [ "exact", "in" ] }
if that doesn't work, you could try:
filtering = {"status": ALL }
and proceed from there. ALL should allow everything so if it didn't work this would mean the problem lies elsewhere.
For more information please take a look at Tastypie docs
go with
filtering = { 'status' : ['exact'] }
now you can use in URL http://localhost:3000/api/v1/candidate-poll/?status=new and the field gets filtered with only json data having status as new