How can I add property of a Model in its JSON made using django.core.serializers to return in AJAX call?
I have a Model:
class MyModel(models.Model):
...
#property
def property_field(self):
return some_value;
Then in views, I send a JSON of this model in an AJAX call. The problem is, I don't know how to send value returned by this property. I tried
query_set = MyModel.objects.flter(...)
serializers.serialize('json', query_set, fields=('...', ... , 'a_property'))
But this doesn't work. How can I pass the value of this property?
Looks like there is no easy way to accomplish this without subclassing your own serializer as the default serializer only goes through the db fields.
Edit:
Previous answer was written a long time ago. There are easier ways to do this now, as expected:
class MyModelSerializer(serializers.ModelSerializer):
property_field = serializers.CharField(
source="property_field",
read_only=True,
)
This should add property_field property in the serailzed OrderedDict which you can return to client
Usage:
serializer = MyModelSerializer(MyModel.objects.all())
return Response(serializer.data)
Old Answer
This JSON Serializer should work:
from StringIO import StringIO
from django.core.serializers.json import Serializer
class JSONSerializer(Serializer):
def serialize(self, queryset, attributes, **options):
self.options = options
self.stream = options.get("stream", StringIO())
self.start_serialization()
self.first = True
for obj in queryset:
self.start_object(obj)
for field in attributes:
self.handle_field(obj, field)
self.end_object(obj)
if self.first:
self.first = False
self.end_serialization()
return self.getvalue()
def handle_field(self, obj, field):
self._current[field] = getattr(obj, field)
(based on this answer, posted by Tim Edgar ^ and changed using this gist)
Usage:
json = JSONSerializer().serialize(modelName.objects.all(), ('attr1', 'property1', ...))
Related
I'm trying to "inject" some raw sql into my DRF nested Serializer:
# SERIALIZERS
class CarSerializer(serializers.ModelSerializer):
class Meta:
model = Car
fields = '__all__'
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer() # <--- here I don't want to get the Car object but rather inject a raw sql.
class Meta:
model = Driver
fields = '__all__'
The SQL injection is needed to request for a specific version of the data since I'm using MariaDB versioning tables but this is not relevant. How do I override the method that gets the object from CarSerializer? Thank you.
This is untested but I think you want to override the __init__ in DriverSerializer and then load the result of your raw SQL via data, something like this:
class DriverSerializer(serializers.ModelSerializer):
[...]
def __init__(self, *args, **kwargs):
super(DriverSerializer, self).__init__(*args, **kwargs)
name_map = {'column_1': 'obj_attr_1', 'column_2': 'obj_attr_1', 'pk': 'id'}
raw = Car.objects.raw('SELECT ... FROM ...', translations=name_map)
data = {k: getattr(raw[0], k) for k in name_map.keys()}
self.car = CarSerializer(data=data)
You could define method under your model to get related Car
class Car(models.Model):
def current_car(self):
return Car.objects.raw('SELECT ... FROM ...')[0]
Then in serializer you could reuse following method
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer(source="current_car")
class Meta:
model = Driver
fields = (...)
Thank you everyone for your answers, I managed to make it work although my solution is not as clean as the one suggested from #yvesonline and #iklinak:
I first checked the official DRF documentation on overriding serializers: https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior
In particular I was interested in the overriding of the method: .to_representation(self, instance) that controls the fetching of the object from the database:
from datetime import datetime as dt
from collections import OrderedDict
from rest_framework.relations import PKOnlyObject
from rest_framework.fields import SkipField, empty
def __init__(
self, instance=None, data=empty, asTime=str(dt.now()), **kwargs):
self.asTime = asTime
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
def to_representation(self, instance):
# substitute instance with my raw query
# WARNING: self.asTime is a custom variable, check the
# __init__ method above!
instance = Car.objects.raw(
'''
select * from db_car
for system_time as of timestamp %s
where id=%s;
''', [self.asTime, instance.id])[0]
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(
attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
You can find the original code here: https://github.com/encode/django-rest-framework/blob/19655edbf782aa1fbdd7f8cd56ff9e0b7786ad3c/rest_framework/serializers.py#L335
Then finally in the DriverSerializer class:
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer(asTime='2021-02-05 14:34:00')
class Meta:
model = Driver
fields = '__all__'
My db model is of events and each event is connected to a venue.
When I retrieve a list of events I use:
venue = VenueSerializer(read_only=True)
When I post to my drf endpoint I use:
venue = serializers.SlugRelatedField(
allow_null=True,
queryset=Venue.objects.all(),
required=False,
slug_field='id')
However this causes that in the response I recieve from the post request, the venue is serialised as a slug field. I want it to use the VenueSerialiser for the response.
I came accross https://stackoverflow.com/a/49035208/5683904 but it only works on the Viewset itself.
#serializer_class = EventSerializer
read_serializer_class = EventSerializer
create_serializer_class = EventCreateUpdateSerializer
I need to build this functionality into the serialiser itself since it is shared with other components.
The Problem
The SlugRelatedField's to_representation method is coded to return the value of the slug_field keyword argument that you pass to it during initialization.
Workarounds
Extend SlugRelatedField and override it's to_representation method to return the complete object instead of the slug. This could be a little tricky because the actual model instance isn't a part of the class.
Have two fields, one for the slug and another for the actual object. This is way easier to implement.
Here's how you can implement the second workaround:
venue = VenueSerializer(read_only=True)
venue_id = serializers.SlugRelatedField(
write_only=True
allow_null=True,
queryset=Venue.objects.all(),
required=False,
slug_field='id')
UPDATE: This is apparently a pretty wanted feature in DRF. I've found a way to implement the first workaround as well. It deals with PrimaryKeyRelatedField but you could probably modify it to work with SlugRelatedField too. Here it is:
from collections import OrderedDict
from rest_framework import serializers
class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
return self.serializer_class(value).data
def get_queryset(self):
if self.queryset:
return self.queryset
return self.serializer_class.Meta.model.objects.all()
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
item.pk,
self.display_value(item)
)
for item in queryset
])
def use_pk_only_optimization(self):
return False
#classmethod
def from_serializer(cls, serializer, name=None, args=(), kwargs={}):
if name is None:
name = f"{serializer.__class__.name}AsymetricAutoField"
return type(name, [cls], {"serializer_class": serializer})
You can then use this field like this:
class FooSerializer(serilizers.ModelSerializer):
bar = AsymetricRelatedField(BarSerializer)
class Meta:
model = Foo
You can find the original discussion about this here
After you create a Django object in a CreateAPI of DRF, you get a status 201 created and the object is returned with the same serializer you used to create the Django object with.
Wanted: on create: Serializer.comments = Textfield(write_only=True)
and on created (201 status) Serializer.comments = a list of commments
I know it's possible by overriding the CreateAPIView.create function.
However, I'd like to know if it's possible by using write_only=True and read_only=True attributes to serializer fields.
For now I think it's not possible because they both have the same name.
I'd have liked to do something like this using a fake kwarg names actual_name:
class CreateEventSerializer(serializers.ModelSerializer):
comments_readonly = serializers.SerializerMethodField(read_only=True, actual_name='comments')
class Meta:
model = Event
fields = ('id', 'comments', 'comments_readonly')
def __init__(self, *args, **kwargs):
super(CreateEventSerializer, self).__init__(*args, **kwargs)
self.fields['comments'].write_only = True
def get_comments_readonly(self, obj):
comments = obj.comments.replace('\r', '\n')
return [x for x in comments.split('\n') if x != '']
But this way, the JSON that is returned still contains the key "comments_readonly" instead of the wanted key "comments".
Using latest DRF, 3.7.1
In other words:
Is it possible to create a serializer field that behaves differently based on read and write, (using only 1 serializer class)?
This seems to do the trick for the JSON response, but it feels a bit hacky, as DRF HTML forms now shows a python list in the comments textarea field.
class CreateEventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ('id', 'comments')
def get_comments(self, obj):
comments = obj.comments.replace('\r', '\n')
return [x for x in comments.split('\n') if x != '']
def to_representation(self, instance):
data = super(CreateEventSerializer, self).to_representation(instance)
data['comments'] = self.get_comments(instance)
return data
I have an ItemCollection and Items in my Django models and I want to be able to remove Items from the collection through the UI. In a REST PUT request I add an extra boolean field deleted for each Item to signal that an Item should be deleted.
The correct way to handle this seems to be in the update method of the Serializer.
My problem is that this non-model deleted field gets removed during validation, so it is not available anymore. Adding deleted as a SerializerMethodField did not help. For now I get my deleted information from the initial_data attribute of the Serializer, but that does not feel right.
My current example code is below. Does anybody know a better approach?
Models:
class ItemCollection(models.Model):
description = models.CharField(max_length=256)
class Item(models.Model):
collection = models.ForeignKey(ItemCollection, related_name="items")
Serializers:
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from models import Item, ItemCollection
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
class ItemCollectionSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=False)
class Meta:
model = ItemCollection
def update(self, instance, validated_data):
instance.description = validated_data['description']
for item, item_obj in zip(
self.initial_data['items'], validated_data['items']):
if item['delete']:
instance.items.filter(id=item['id']).delete()
return instance
class ItemCollectionView(APIView):
def get(self, request, ic_id):
item_collection = get_object_or_404(ItemCollection, pk=ic_id)
serialized = ItemCollectionSerializer(item_collection).data
return Response(serialized)
def put(self, request, ic_id):
item_collection = get_object_or_404(ItemCollection, pk=ic_id)
serializer = ItemCollectionSerializer(
item_collection, data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data)
And an example of the json in the PUT request:
{
"id": 2,
"items": [
{
"id": 3,
"collection": 2,
"delete": true
}
],
"description": "mycoll"
}
You can add non-model fields back by overwriting the to_internal_value fn:
def to_internal_value(self, data):
internal_value = super(MySerializer, self).to_internal_value(data)
my_non_model_field_raw_value = data.get("my_non_model_field")
my_non_model_field_value = ConvertRawValueInSomeCleverWay(my_non_model_field_raw_value)
internal_value.update({
"my_non_model_field": my_non_model_field_value
})
return internal_value
Then you can process it however you want in create or update.
If you're doing a PUT request, your view is probably calling self.perform_update(serializer). Change it for
serializer.save(<my_non_model_field>=request.data.get('<my_non_model_field>', <default_value>)
All kwargs are passed down to validated_data to your serializer.
Make sure to properly transform incoming value (to boolean, to int, etc.)
I have a simple model MyModel with a date field named publication_date. I also have a custom manager that filters my model based on this date field.
This custom manager is accessible by .published and the default one by .objects.
from datetime import date, datetime
from django.db import models
class MyModelManager(models.Manager):
def get_query_set(self):
q = super(MyModelManager, self).get_query_set()
return q.filter(publication_date__lte=datetime.now())
class MyModel(models.Model):
...
publication_date = models.DateField(default=date.today())
objects = models.Manager()
published = MyModelManager()
This way, I got access to all objects in the admin but only to published ones in my views (using MyModel.published.all() queryset).
I also have
def get_previous(self):
return self.get_previous_by_publication_date()
def get_next(self):
return self.get_next_by_publication_date()
which I use in my templates: when viewing an object I can link to the previous and next object using
{{ object.get_previous }}
The problem is: this returns the previous object in the default queryset (objects) and not in my custom one (published).
I wonder how I can do to tell to this basic model functions (get_previous_by_FOO) to use my custom manager.
Or, if it's not possible, how to do the same thing with another solution.
Thanks in advance for any advice.
Edit
The view is called this way in my urlconf, using object_detail from the generic views.
(r'^(?P<slug>[\w-]+)$', object_detail,
{
'queryset': MyModel.published.all(),
'slug_field': 'slug',
},
'mymodel-detail'
),
I'm using Django 1.2.
In fact, get_next_or_previous_by_FIELD() Django function (which is used by get_previous_by_publication_date...) uses the default_manager.
So I have adapted it to reimplement my own utility function
def _own_get_next_or_previous_by_FIELD(self, field, is_next):
if not self.pk:
raise ValueError("get_next/get_previous cannot be used on unsaved objects.")
op = is_next and 'gt' or 'lt'
order = not is_next and '-' or ''
param = smart_str(getattr(self, field.attname))
q = Q(**{'%s__%s' % (field.name, op): param})
q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk})
qs = MyModel.published.filter(q).order_by('%s%s' % (order, field.name), '%spk' % order)
try:
return qs[0]
except IndexError:
def get_previous(self):
return self._own_get_next_or_previous_by_FIELD(MyModel._meta.fields[4], False)
def get_next(self):
return self._own_get_next_or_previous_by_FIELD(MyModel._meta.fields[4], True)
This is not a very clean solution, as I need to hardcode the queryset and the field used, but at least it works.