How to apply a transformation on a field when serializing? - python

In Django, how does one apply a custom transformation while serializing a field?
For instance, I have a model which has a geometry field, which is stored in a specific coordinate system. Now, for this one serializer, I'd like to perform a conversion that converts the coordinates to another coordinate system. How is that done?
The serializer currently looks like this:
class LinkWithGeometrySerializer(serializers.ModelSerializer):
class Meta:
model = Link
fields = ['link_type',
'geometry',
]
The geometry is the field that should have a transformation applied to it.

As Iklinac pointed out, you can use a custom field, but that only pays off when you can reuse it.
There's two other common approaches:
Keep it at serializer level:
class LinkWithGeometrySerializer(serializers.ModelSerializer):
geometry = serializers.SerializerMethodField()
class Meta:
model = Link
fields = ['link_type', 'geometry',]
#staticmethod
def get_geometry(obj: Link):
# for example obtain srid from context, by passing it in via view or hardcode
return obj.geometry.transform(srid=your_srid)
At the model level (make the database do the transformation):
in your view:
from django.contrib.gis.db.models.functions import Transform
TARGET_SRID = 4326
class LinkView(RetrieveAPIVIew):
queryset = Link.objects.annotate(transformed=Transform("geometry", TARGET_SRID))
...
or (srid passed as path component in url):
from django.contrib.gis.db.models.functions import Transform
class LinkView(RetrieveAPIVIew):
def get_queryset(self):
return Link.objects.annotate(transformed=Transform("geometry", self.kwargs["srid"])
serializer
class LinkWithGeometrySerializer(serializers.ModelSerializer):
geometry = serializers.GeometryField(source='transformed') # [1]
class Meta:
model = Link
fields = ['link_type', 'geometry',]
[1] https://github.com/openwisp/django-rest-framework-gis/blob/master/rest_framework_gis/fields.py#L13

Related

Django admin inline conditioned on model field values

I'm trying to show one of three different child model inlines for a parent model, based on a selected field value within the parent model. I have tried every solution that pops up from various google searches but nothing seems to work.
First a little background, I'm new to python and django but so far I feel that I have been a quick study. I'm attempting to build a web application to house information linked to various spatial locations. The geometry type (geom_type) for each location may be different (i.e., points, linestring, and polygons are possible). To capture this information I plan to create a parent model (Location) to house the name and geom_type (and possibly other metadata). The spatial data related to each Location would then be housed in three separate child models; one for each geom_type. When entering data I would like to create a new location and select the geom_type, which would then pull up the correct inline.
Now for the details:
Models
from django.contrib.gis.db import models
class Geometry(models.Model):
TYPE = (
('Point', 'Point'),
('Linestring', 'Linestring'),
('Polygon', 'Polygon'),
)
geom_type = models.CharField('Geometry Type', choices = TYPE, max_length = 30)
class Meta:
verbose_name = 'Geometry'
verbose_name_plural = 'Geometries'
def __str__(self):
return self.geom_type
class Location(models.Model):
name = models.CharField('Location Name', max_length = 50)
geom_type = models.ForeignKey(Geometry, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Point(models.Model):
name = models.OneToOneField(Location, on_delete=models.CASCADE)
geometry = models.PointField()
def __str__(self):
return self.name.name
class Linestring(models.Model):
name = models.OneToOneField(Location, on_delete=models.CASCADE)
geometry = models.LineStringField()
def __str__(self):
return self.name.name
class Polygon(models.Model):
name = models.OneToOneField(Location, on_delete=models.CASCADE)
geometry = models.PolygonField()
def __str__(self):
return self.name.name
Admin
from django.contrib.gis import admin
from leaflet.admin import LeafletGeoAdmin, LeafletGeoAdminMixin
from .models import Geometry, Location, Point, Linestring, Polygon
class GeometryAdmin(admin.ModelAdmin):
list_display = ('id', 'geom_type')
admin.site.register(Geometry, GeometryAdmin)
class PointInline(LeafletGeoAdminMixin, admin.StackedInline):
model = Point
class LinestringInline(LeafletGeoAdminMixin, admin.StackedInline):
model = Linestring
class PolygonInline(LeafletGeoAdminMixin, admin.StackedInline):
model = Polygon
class LocationAdmin(admin.ModelAdmin):
model = Location
list_display = ('id', 'name', 'geom_type')
inlines = [
PointInline,
LinestringInline,
PolygonInline
]
admin.site.register(Location, LocationAdmin)
All three inlines show up correctly with the code above as expected. However, when I try to incorporate the conditional logic with different variations of get_inlines or get_inline_instances it always just ends up displaying the inline associated with the final "else" statement.
My failed attempt
def get_inlines(self, request, obj: Location):
if obj.geom_type == 'Point':
return [PointInline]
elif obj.geom_type == 'Location':
return [LinestringInline]
elif obj.geom_type == 'Polygon':
return [PolygonInline]
else:
return []
I believe the problem occurs because conditional statements are not referencing the model field correctly. But I can't seem to stumble upon the correct way to achieve my expected outcome.
Use related_name in model like below:
next_question = models.ForeignKey(Question, on_delete=models.CASCADE, null = True, blank = True, related_name='next_question', limit_choices_to={'is_active': True})
and then fk_name Like the example below: Then try. Hope you can find a solution by yourself.
class Labels(admin.TabularInline):
model = Label
extra = 0
fk_name = "next_question"
Use admin.StackedInline for OneToOne and admin.TabularInline for ForeignKey.
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
Create separate admin for 'Geometry' and 'Location' if you stacked.

Django rest framework nested serializer create method

I have created a nested serializer, when I try to post data in it it keeps on displaying either the foreign key value cannot be null or dictionary expected. I have gone through various similar questions and tried the responses but it is not working for me. Here are the models
##CLasses
class Classes(models.Model):
class_name = models.CharField(max_length=255)
class_code = models.CharField(max_length=255)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.class_name
class Meta:
ordering = ['class_code']
##Streams
class Stream(models.Model):
stream_name = models.CharField(max_length=255)
classes = models.ForeignKey(Classes,related_name="classes",on_delete=models.CASCADE)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.stream_name
class Meta:
ordering = ['stream_name']
Here is the view
class StreamViewset(viewsets.ModelViewSet):
queryset = Stream.objects.all()
serializer_class = StreamSerializer
Here is the serializer class
class StreamSerializer(serializers.ModelSerializer):
# classesDetails = serializers.SerializerMethodField()
classes = ClassSerializer()
class Meta:
model = Stream
fields = '__all__'
def create(self,validated_data):
classes = Classes.objects.get(id=validated_data["classes"])
return Stream.objects.create(**validated_data, classes=classes)
# def perfom_create(self,serializer):
# serializer.save(classes=self.request.classes)
#depth = 1
# def get_classesDetails(self, obj):
# clas = Classes.objects.get(id=obj.classes)
# classesDetails = ClassSerializer(clas).data
# return classesDetails
I have tried several ways of enabling the create method but like this displays an error {"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}. Any contribution would be deeply appreciated
This is a very common situation when developing APIs with DRF.
The problem
Before DRF reaches the create() method, it validates the input, which I assume has a form similar to
{
"classes": 3,
"stream_name": "example"
}
This means that, since it was specified that
classes = ClassSerializer()
DRF is trying to build the classes dictionary from the integer. Of course, this will fail, and you can see that from the error dictionary
{"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}
Solution 1 (requires a new writable field {field_name}_id)
A possible solution is to set read_only=True in your ClassSerializer, and use an alternative name for the field when writing, it's common to use {field_name}_id. That way, the validation won't be done. See this answer for more details.
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
'classes_id',
)
extra_kwargs = {
'classes_id': {'source': 'classes', 'write_only': True},
}
This is a clean solution but requires changing the user API. In case that's not an option, proceed to the next solution.
Solution 2 (requires overriding to_internal_value)
Here we override the to_internal_value method. This is where the nested ClassSerializer is throwing the error. To avoid this, we set that field to read_only and manage the validation and parsing in the method.
Note that since we're not declaring a classes field in the writable representation, the default action of super().to_internal_value is to ignore the value from the dictionary.
from rest_framework.exceptions import ValidationError
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
def to_internal_value(self, data):
classes_pk = data.get('classes')
internal_data = super().to_internal_value(data)
try:
classes = Classes.objects.get(pk=classes_pk)
except Classes.DoesNotExist:
raise ValidationError(
{'classes': ['Invalid classes primary key']},
code='invalid',
)
internal_data['classes'] = classes
return internal_data
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
)
With this solution you can use the same field name for both reading and writing, but the code is a bit messy.
Additional notes
You're using the related_name argument incorrectly, see this question. It's the other way around,
classes = models.ForeignKey(
Classes,
related_name='streams',
on_delete=models.CASCADE,
)
In this case it should be streams.
Kevin Languasco describes the behaviour of the create method quite well and his solutions are valid ones. I would add a variation to solution 1:
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
classes_id = serializers.IntegerField(write_only=True)
def create(self,validated_data):
return Stream.objects.create(**validated_data, classes=classes)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'classes_id',
'created_date',
)
The serializer will work without overriding the create method, but you can still do so if you want to as in your example.
Pass the value classes_id in the body of your POST method, not classes. When deserializing the data, the validation will skip classes and will check classes_id instead.
When serializing the data (when you perform a GET request, for example), classes will be used with your nested dictionary and classes_id will be omitted.
You can also solve this issue in such a way,
Serializer class
# Classes serializer
class ClassesSerializer(ModelSerializer):
class Meta:
model = Classes
fields = '__all__'
# Stream serializer
class StreamSerializer(ModelSerializer):
classes = ClassesSerializer(read_only=True)
class Meta:
model = Stream
fields = '__all__'
View
# Create Stream view
#api_view(['POST'])
def create_stream(request):
classes_id = request.data['classes'] # or however you are sending the id
serializer = StreamSerializer(data=request.data)
if serializer.is_valid():
classes_instance = get_object_or_404(Classes, id=classes_id)
serializer.save(classes=classes_instance)
else:
return Response(serializer.errors)
return Response(serializer.data)

How to get all instances in serializer method field

How to get all instances in serializer method field
I have a serializer method field and I am passing list data in the form of context to serializer like below.
name_list = [ "abc", "def",....]
obj_list = abc.objects.all()
Serializer = abcSerializer (obj_list, context=name_list, many=True)
class abcSerializer (serializers.ModelSerializer):
xyz = serializers.SerializerMethodField ("getXYZ", read_only=True)
class Meta:
model = abc
def getXYZ (self, data):
# here I want all instanceses, but I got only one instance in data.
I want to attach name_list data one by one to instace data with same index?
How I can get all instanceses in my serializer method field?
Why do you need all instances? If you want to manipulate something in all instances, better do it before passing it as argument in Serializer. If you want to get indivisual instance, you should get the value in data parameter. But your indentations are wrong. Try like this:
class abcSerializer (serializers.ModelSerializer):
xyz = serializers.SerializerMethodField("getXYZ")
class Meta:
model = abc
def getXYZ(self, data):
print(data) # it will print a instance of abc
return value_based_on_data
Update
Then I think you should try like this:
First update serializer class:
class abcSerializer (serializers.ModelSerializer): # use PascalCase for naming classes
xyz = serializers.ReadOnlyField()
class Meta:
model = abc
fields = '__all__' # use PascalCase for naming classes
Then use the following code to get values of xyz:
obj_list = []
for i, item in enumerate(abc.objects.all()):
item.xyz = name_list[i]
obj_list.append(item)
abcSerializer(obj_list, many=True).data
After dig deep into debugging I realized that I should share my findings with the community.
Look at the below line:
Serializer = abcSerializer (obj_list, context=name_list, many=True)
Here many=True make's abcSerializer to list serializer, according to rest framework documentation in list serializer we can access all objects of queryset in update method like below
class BookListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
# Maps for id->instance and id->data item.
book_mapping = {book.id: book for book in instance}
data_mapping = {item['id']: item for item in validated_data}
# Perform creations and updates.
I found that we can also access all objects of queryset in any method even in serializerMethodField using below syntax
def getXYZ (self, data):
objects = self.instance

how to make django's choice option dynamic in the model

I am creating a reference lexicon in django. The lexicon is a translator for certain fields from one language to another.
The lexicon goes like this: 'id','lng1','lng2','lng3'
I have a model that uses the lexicon in one of the fields.
class lexicon(models.Model):
lng1 = model.Charfield(max_length=255)
lng2 = model.Charfield(max_length=255)
lng3 = model.Charfield(max_length=255)
lexicon_choices = (
(lng1=lng1),
(lng2=lng2),
(lng3=lng3)
)
class model(models:Model):
model_name = model.Charfield(max_length=255, unique)
field1 = model.Charfield(max_length=3,CHOICES=lexicon_choices)
This works ok, but I want to know, is there a way to glean out the column names from class lexicon and port them as the choices for class model? I am using Django Rest Framework for the first time, and I want to make sure the right lng is there.
Thanks
You can use a ForeignKey on the lexicon class to your main model. This will ensure a One to Many relationship. And you can store the values in database.
yes, possible. but in admin.py
class CustomForm(forms.ModelForm):
class Meta:
model = YourModel
def __init__(self, *args, **kwargs):
super(CustomForm, self).__init__(*args, **kwargs)
choices = [(x.id, x.name) for x in Lexicon.objects.all()]
self.fields['field1'].choices = choices
class YourModelAdmin(admin.ModelAdmin):
form = CustomForm
and your original model looks like:
class YourModel(models:Model):
model_name = model.Charfield(max_length=255, unique)
field1 = models.CharField(max_length=10, choices=(('--', '--'),))
IMPORTANT: do not name your class names with lower case. ;)

django rest framework and model inheritance

I have an "abstract" model class MyField:
class MyField(models.Model):
name = models.CharField(db_index = True, max_length=100)
user = models.ForeignKey("AppUser", null=False)
I have a few other subclasses of MyField each defining a value of a specific type.
for example:
class MyBooleanField(MyField):
value = models.BooleanField(db_index = True, default=False)
In MyField I have a method get_value() that returns the value based on the specific subclass.
In django rest I want to fetch all the fields of a user
class AppUserSerializer(serializers.ModelSerializer):
appuserfield_set = MyFieldSerializer(many=True)
class Meta:
model = AppUser
fields = ('appuser_id', 'appuserfield_set')
On the client side I want the user to be able to add new fields and set values to them and then on the server I want to be able to create the correct field based on the value.
What is the correct way to achieve this behavior?
After some digging, here is what I ended up doing. Aside from the code below I had to implement get_or_create and create the relevant subclass of MyField based on the passed value.
class ValueField(serializers.WritableField):
#called when serializing a field to a string. (for example when calling seralizer.data)
def to_native(self, obj):
return obj;
"""
Called when deserializing a field from a string
(for example when calling is_valid which calles restore_object)
"""
def from_native(self, data):
return data
class MyFieldSerializer(serializers.ModelSerializer):
value = ValueField(source='get_value', required=False)
def restore_object(self, attrs, instance=None):
"""
Called by is_valid (before calling save)
Create or update a new instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.user = attrs.get('user', instance.user)
instance.name = attrs.get('name', instance.name)
else:
# Create new instance
instance = MyField.get_or_create(end_user=attrs['user'],
name=attrs['name'],
value=attrs['get_value'])[0]
instance.value = attrs['get_value']
return instance
def save_object(self, obj, **kwargs):
#called when saving the instance to the DB
instance = MyField.get_or_create(end_user=obj.user,
name=obj.name,
value=obj.value)[0]
class Meta:
model = MyField
fields = ('id', 'user', 'name', 'value')

Categories

Resources