Django Rest Framework - How to nest several fields in a serializer? - django

I have several a base model with several control fields. Among them a location fields compound from lat, lon, accuracy, provider and client time. Most of my writable models (and hence resources) are inheriting from this base model.
I'm trying to make DRF serialize the location related fields in a nested "location" field. For example,
{
"id": 1,
"name": "Some name",
"location": {
"lat": 35.234234,
"lon": 35.234234,
"provider": "network",
"accuracy": 9.4,
}
}
I'ts important to remember that these fields are regular (flat) fields on the base model.
I've investigated and found several options
Create a custom field and by overriding "get_attribute" create the nested representation. I don't like this solution because i lose some of the benefits of the model serializer such as validation.
Create a nested resource called Location. I guess i could make it work by adding a property by the same name on the model but again, no validations.
So my question is, What is the best way to nest ( or group ) several fields in a DRF serializer ?
DRF 3.0.0, Django 1.7
EDIT:
Building on top of #Tom Christie answer this is what i came up with (simplified)
# models.py
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
lat = models.FloatField(blank=True, null=True)
lon = models.FloatField(blank=True, null=True)
location_time = models.DateTimeField(blank=True, null=True)
location_accuracy = models.FloatField(blank=True, null=True)
location_provider = models.CharField(max_length=50, blank=True, null=True)
#property
def location(self):
return {
'lat': self.lat,
'lon': self.lon,
'location_time': self.location_time,
'location_accuracy': self.location_accuracy,
'location_provider': self.location_provider
}
class ChildModel(BaseModel):
name = models.CharField(max_lengtg=10)
# serializers.py
class LocationSerializer(serializers.Serializer):
lat = serializers.FloatField(allow_null=True, required=False)
lon = serializers.FloatField(allow_null=True, required=False)
location_time = serializers.DateTimeField(allow_null=True, required=False)
location_accuracy = serializers.FloatField(allow_null=True, required=False)
location_provider = serializers.CharField(max_length=50,allow_null=True, required=False)
class BaseSerializer(serializers.ModelSerializer):
def create(self,validated_data):
validated_data.update(validated_data.pop('location',{}))
return super(BaseSerializer,self).create(validated_data)
def update(self, instance, validated_data):
location = LocationSerializer(data=validated_data.pop('location',{}), partial=True)
if location.is_valid():
for attr,value in location.validated_data.iteritems():
setattr(instance,attr,value)
return super(BaseSerializer,self).update(instance, validated_data)
class ChildSerializer(BaseSerializer):
location = LocationSerializer()
class meta:
model = ChildModel
fields = ('name','location',)
I've tested with valid/invalid post/patch and it worked perfectly.
Thanks.

I'd suggest simply using explicit serializer classes, and writing the fields explicitly. It's a bit more verbose, but it's simple, obvious and maintainable.
class LocationSerializer(serializers.Serializer):
lat = serializers.FloatField()
lon = serializers.FloatField()
provider = serializers.CharField(max_length=100)
accuracy = serializers.DecimalField(max_digits=3, decimal_places=1)
class FeatureSerializer(serializers.Serializer):
name = serializers.CharField(max_length=100)
location = LocationSerializer()
def create(self, validated_data):
return Feature.objects.create(
name=validated_data['name'],
lat=validated_data['location']['lat'],
lon=validated_data['location']['lat'],
provider=validated_data['location']['provider'],
accuracy=validated_data['location']['accuracy']
)
def update(self, instance, validated_data):
instance.name = validated_data['name']
instance.lat = validated_data['location']['lat']
instance.lon = validated_data['location']['lat']
instance.provider = validated_data['location']['provider']
instance.accuracy = validated_data['location']['accuracy']
instance.save()
return instance
There's a bunch of ways you could use a ModelSerializer instead, or ways to keep the create and update methods a little shorter, but it's not clear that the extra indirection you'd be giving yourself is at all worth it.
We almost always use completely explicit serializer classes for APIs that we're building.

Related

unique_together does not prevent duplicate records

I have two models like this :
class Preference(models.Model):
lat = models.DecimalField(max_digits=25,decimal_places=15)
lng = models.DecimalField(max_digits=25,decimal_places=15)
name = models.CharField(max_length=150,null=True)
address = models.TextField(max_length=350,null=True)
type = models.CharField(max_length=150,blank=True,null=True)
class PrefenceOfUser(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
place_detail = models.ForeignKey(Preference, on_delete=models.CASCADE)
def __str__(self):
return self.user.username
class Meta:
unique_together = ('user', 'place_detail',)
this is the json i post to my apiview :
{
"lat": "29.621142463088336",
"lng": "52.520185499694527",
"name":"cafesama1",
"address":"streetn1",
"type":"cafe"
}
in views.py :
class PreferedLocationsOfUsers(APIView):
def post(self, request, format=None):
serializer = PreferLocationSerializer(data=request.data)
if serializer.is_valid():
location= Preference(**serializer.data)
location.save()
user_perefeces = PrefenceOfUser(user=request.user,place_detail=location)
user_perefeces.save()
return Response({'location saved'},status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
i want to prevent the user to save dublicated records in database but when location object is saved in PrefenceOfUser unique_together does not prevent dublicating. any idea why?
You do have a migration issue as per the comments. Get rid of the duplicates, re-run the migration until you get no errors and you're fine.
However, I would turn this around as a model design problem. You shouldn't need to resort to manual constraints for this.
class Preference(models.Model):
lat = models.DecimalField(max_digits=25,decimal_places=15)
lng = models.DecimalField(max_digits=25,decimal_places=15)
name = models.CharField(max_length=150,null=True)
address = models.TextField(max_length=350,null=True)
type = models.CharField(max_length=150,blank=True,null=True)
users = models.ManyToManyField(get_user_model(), blank=True)
This above code will have exactly the same implications as yours, is cleaner and more Djangoesque.
You can still access what you call PrefenceOfUser through Preference.users.through. If you plan to add more attributes to the selection (ie. when did user add their preference), you can just do:
class PrefenceOfUser(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
place_detail = models.ForeignKey(Preference, on_delete=models.CASCADE)
extra_field = models.WhateverField()
def __str__(self):
return self.user.username
and change the Preference.users to
models.ManyToManyField(get_user_model(), through=PrefenceOfUser)
which will still still ensure the uniqueness.

serializing image data from diffrent fields

From my little knowledge on how serializers work, I know we mostly use modelserializers, and for that, we would have a model for all we want to serialize but how can I join all the images in the different models and then serialize them.
These are my models
class Vendors(models.Model):
title = models.CharField(max_length=50, blank=False)
image = models.ImageField(upload_to='static/vendors', blank=True, null=True)
class Riders(models.Model):
title = models.CharField(max_length=50, blank=False)
image = models.ImageField(upload_to='static/riders', blank=True, null=True)
class VendorsRiders(models.Model):
VendorImg = models.ForeignKey(Vendors, on_delete=models.CASCADE)
RiderImg = models.ForeignKey(Riders, on_delete=models.CASCADE)
This is my serializer
Class VendorsRidersSerializers(models.Model):
Class Meta:
model = VendorsRiders
fields = '__all__'
So, how to get all the images to the endpoint i would specify ? since, I'm a beginner in DRF I also need a suggestion and advice on the best practice to do this. Thank you
With defining fields = '__all__' in a serializer you just have access to properties of your model. One way to serialize your image fields of related models is to define SerializerMethodField Like:
Class VendorsRidersSerializers(models.Model):
rider_image = serializers.SerializerMethodField()
Class Meta:
model = VendorsRiders
fields = '__all__'
def get_rider_image(self, obj):
req = self.context['request']
# build_absolute_uri will convert your related url to absolute url
return req.build_absolute_uri(obj.VendorImg.image.url)
Also note that you should pass the request from your view to context of your serializer to build an absolute url (Example: VendorsRidersSerializers(queryset, many=True, context={'request': self.request}) or override the get_serializer_class method and pass the request to it's context) and then use the data of this serializer.

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

Two step object creation in Django Admin

I'm trying to change the implementation of an EAV model using a JSONField to store all the attributes defined by an attribute_set.
I already figured out how to build a form to edit the single attributes of the JSON, but I'm currently stuck at implementing the creation of a new object. I think I have to split object creation in two steps, because I need to know the attribute_set to generate the correct form, but I don't know if there's a way to hook in the create action, or any other way to achieve what I need.
My models look like this:
class EavAttribute(models.Model):
entity_type = models.CharField(max_length=25, choices=entity_types)
code = models.CharField(max_length=30)
name = models.CharField(max_length=50)
data_type = models.CharField(max_length=30, choices=data_types)
class AttributeSet(models.Model):
name = models.CharField(max_length=25)
attributes = models.ManyToManyField('EavAttribute')
class EntityAbstract(models.Model):
attribute_set = models.ForeignKey(
'AttributeSet',
blank=False,
null=False,
unique=False,
)
class Meta:
abstract = True
class Event(EntityAbstract):
entity_type = models.CharField(max_length=20, null=False, choices=entity_types, default=DEFAULT_ENTITY_TYPE)
code = models.CharField(max_length=25, null=True, blank=True, db_index=True)
year = models.IntegerField(db_index=True)
begin_date = models.DateField()
end_date = models.DateField()
data = JSONField()
How can I choose the AttributeSet first and then go to another form that I would populate with the attributes in the chosen attribute set?
I ended up using get_fields() and response_add() methods, like so:
def get_fields(self, request, obj=None):
if obj is None:
return ['attribute_set']
else:
return [attr.name for attr in obj._meta.get_fields() if not attr.auto_created and attr.name != 'id']
def get_readonly_fields(self, request, obj=None):
readonly_fields = ['entity_type', 'code', 'state']
if obj is not None:
readonly_fields.append('attribute_set')
return readonly_fields
def response_add(self, request, obj, post_url_continue=None):
url = '/admin/risks/event/{}/change/'.format(obj.id)
return redirect(url)
The downside of this approach is that object is saved in the database and then opened for edit, so basically the database is hit twice and all attributes have to be nullable, except for attribute_set.
I would be happy to receive ideas for better implementations.

django drf left join

I have this model:
class Env(models.Model):
env_name = models.CharField(max_length=100, unique=True)
is_enabled = models.CharField(max_length=1, choices=ENABLED, default='Y')
def __unicode__(self):
return unicode(self.env_name)
I also have this model ...
class Hosts(models.Model):
host_name = models.CharField(max_length=200, unique=True)
host_variables = jsonfield.JSONField()
host_env = models.ForeignKey(Env, models.DO_NOTHING, related_name='host_env')
I wish to have a serialized representation equivalent to a join.
I'm trying to get rows that contain host_name and env_name
I can't seem to find the right way to serialize it
I'm have so far ...
class HostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Hosts
fields = ('host_name', 'ip_address', 'is_enabled','is_managed','managed_users')
I can't seem to find the right way to get the name of the env in each row of my Hosts results.
What am I missing?
A serializer only handles a single Model, so anything else you want to add has to be added explicitly.
If you just want to add the env_name, you can use the SerializerMethodField field like this:
class HostSerializer(serializers.HyperlinkedModelSerializer):
env_name = serializers.SerializerMethodField()
class Meta:
model = Hosts
fields = ('host_name', 'env_name', 'ip_address', 'is_enabled','is_managed',
'managed_users',)
def get_env_name(self, obj):
host_env = obj.host_env
if host_env:
return str(host_env.env_name)
return None
Note that you may also want to look into nested serializers, but that would produce something like:
{
'host_name': 'my host name',
'host_env': {
'env_name': 'My env name'
}
}
See http://www.django-rest-framework.org/api-guide/relations/#nested-relationships for that (not explaining that as that was not your OP, but giving it to you as a reference for a potentially better way)
You can try
class HostSerializer(serializers.HyperlinkedModelSerializer):
env_name = serializers.ReadOnlyField(source='host_env.env_name')
class Meta:
model = Hosts
fields = ('host_name', 'ip_address', 'is_enabled','is_managed','managed_users', 'env_name',)