I have the following SerializerField:
class TimestampField(Field):
def to_representation(self, value):
if not value:
return ''
return value.timestamp()
And I use it like this in my serializer:
class ArticlePhotobookSerializer(ModelSerializer):
delivery_date_from = TimestampField()
delivery_date_to = TimestampField()
Now the getter delivery_date_to can return None, which I want to transform into an empty string using the to_representation method. however, when I use the Serializer to parse this None value, it doesn't even enter the to_representation method and immediately returns None. What should I change to also use the method to_representation for None?
By default serializer's to_representation method skip fields with None value (see source).
You can write mixin class to override default to_representation:
class ToReprMixin(object):
def to_representation(self, instance):
ret = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
ret[field.field_name] = field.to_representation(attribute)
return ret
and use it in your serializers:
class ArticlePhotobookSerializer(ToReprMixin, ModelSerializer):
...
If you would like to change the result of to_representation when there is no instance (not exactly the same problem as you had, but matches the question title), to_representation will not even be called in DRF v3. One can change the result by subclassing the get_initial method:
def get_initial(self):
"""
Return a value to use when the field is being returned as a primitive
value, without any object instance.
"""
if callable(self.initial):
return self.initial()
return self.initial
Heres an example:
def get_initial(self) -> dict:
return {'display_name': 'moocows'}
Here we use the context as initial representation:
def get_initial(self) -> dict:
return self.context
Related
I'm unit testing a view and I am attempting to patch the .data property on my serializer but it looks like it behaves differently when the many=True kwarg is passed to the serializer constructor and thus not properly patching. Here is a generalized example of my code.
# myapp/serializers.py
class MySerializer(serializers.Serializer):
some_field = serializers.CharField()
# myapp/views.py
class MyView(View):
def get(self, request):
# ..stuff
some_data = []
serializer = MySerializer(some_data, many=True)
print(type(serializer)) # <class 'rest_framework.serializers.ListSerializer'>
print(type(serializer.data)) # <class 'rest_framework.utils.serializer_helpers.ReturnList'>
return Response({"data": seralizer.data, status=200})
# in tests
def test_view_case_one(mocker):
# setup other mocks
serialized_data = mocker.patch("myapp.views.MySerializer.data", new_callable=mocker.PropertyMock)
# invoke view
response = MyView().get(fake_request)
# run assertions
serialized_data.assert_called_once() # this says it's never called
Earlier I had ran into issues attempting to patch rest_framework.serializers.ListSerializer.data. Must of been a typo. Reattempted and was able to successfully patch. Given the case many=True recreates the serializer as a ListSerializer I simply needed to patch the property on the underlying class.
serialized_data = mocker.patch(
"rest_framework.serializers.ListSerializer.data",
new_callable=mocker.PropertyMock
)
Edit: A more in depth answer
When many=True is used the __new__ method on BaseSerializer grabs you class and constructs a ListSerializer from it and that is why my object showed up as a ListSerializer. Since we are actually receiving a ListSerializer instead of our defined class the patch is not applied to ListSerializer.data method. The relevant parts of the source code for BaseSerializer is below
class BaseSerializer(Field):
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
#classmethod
def many_init(cls, *args, **kwargs):
"""
This method implements the creation of a `ListSerializer` parent
class when `many=True` is used. You can customize it if you need to
control which keyword arguments are passed to the parent, and
which are passed to the child.
Note that we're over-cautious in passing most arguments to both parent
and child classes in order to try to cover the general case. If you're
overriding this method you'll probably want something much simpler, eg:
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
return CustomListSerializer(*args, **kwargs)
"""
allow_empty = kwargs.pop('allow_empty', None)
child_serializer = cls(*args, **kwargs)
list_kwargs = {
'child': child_serializer,
}
if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
})
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs)
In my BookSerializer, I have a nested field page:
class PageSerializer(serializers.ModelSerializer):
...
class BookSerializer(serializers.ModelSerializer):
page = PageSerializer()
and the page field validator expects an dictionary as value. But what I want is it should accept an integer as well (page's id). So in the BookSerializer, I tried to override the validate function for the page field but it didn't work:
class BookSerializer(serializers.ModelSerializer):
page = PageSerializer()
def validate_page(self, value):
if isinstance(value, int):
return value
# if value is not an integer, reuse the default validator
# but django said that validate_page is not a function
return super().validate_page()
Seems like the validate_page function is never called because it's a nested field.
Thanks !
Correct way to create custom validation is:
def validate_page(self, value):
if isinstance(value, int):
return value
return value
But it won't be working.
You need override to_internal function on Page serializer:
class PageSerializer(serializers.ModelSerializer):
def to_internal_value(self, data):
return get_object_or_404(Page, pk=data)
...
class BookSerializer(serializers.ModelSerializer):
page = PageSerializer()
I have tried to add a key serializer.data['test'] = 'asdf', this does not appear to do anything.
I want to transform the representation of a key's value. To do this, I'm trying to use the value to calculate a new value and replace the old one in the dictionary.
This is what I want to accomplish, but I don't know why the value is not being replaced. There are no errors thrown, and the resulting dictionary has no evidence that I've tried to replace anything:
class PlaceDetail(APIView):
def get(self, request, pk, format=None):
place = Place.objects.select_related().get(pk=pk)
serializer = PlaceSerializer(place)
#serializer.data['tags'] = pivot_tags(serializer.data['tags'])
serializer.data['test'] = 'asdf'
print(serializer.data['test'])
return Response(serializer.data)
Terminal: KeyError: 'test'
I have observed by printing that serializer.data is a dictionary.
I have also tested that the syntax I'm trying to use should work:
>>> test = {'a': 'Alpha'}
>>> test
{'a': 'Alpha'}
>>> test['a']
'Alpha'
>>> test['a'] = 'asdf'
>>> test
{'a': 'asdf'}
How can I properly modify the serializer.data dictionary?
The Serializer.data property returns an OrderedDict which is constructed using serializer._data. The return value is not serializer._data itself.
Thus changing the return value of serializer.data does not change serializer._data member. As a consequence, the following calls to serializer.data are not changed.
# In class Serializer(BaseSerializer)
#property
def data(self):
ret = super(Serializer, self).data
return ReturnDict(ret, serializer=self)
# In class ReturnDict(OrderedDict)
def __init__(self, *args, **kwargs):
self.serializer = kwargs.pop('serializer')
super(ReturnDict, self).__init__(*args, **kwargs)
You can keep a copy of the return value of serializer.data, which is an ordered dictionary, and manipulate it as you wish.
Example:
# keep the return value of serializer.data
serialized_data = serializer.data
# Manipulate it as you wish
serialized_data['test'] = 'I am cute'
# Return the manipulated dict
return Response(serialized_data)
Why:
If you look at the source code of Django Restframework, you will see that in Serializer class,
Serializer._data is just a normal dictionary.
Serializer.data is a method decorated to act like a property. It returns a ReturnDict object, which is a customized class derived from OrderedDict. The returned ReturnDict object is initialized using key/value pairs in Serializer._data.
If Serializer.data returns Serializer._data directly, then your original method will work as you expected. But it won't work since it's returning another dictionary-like object constructed using Serializer._data.
Just keep in mind that the return value of Serializer.data is not Serializer._data, but an ordered dictionary-like object. Manipulating the return value does not change Serializer._data.
I believe the reason why serializer.data does not return serializer._data directly is to avoid accidental change of the data and to return a pretty representation of serializer._data.
You'll want to use SerializerMethodField instead of explicitly overwrite the representations.
Building further on #yuwang's answer, I used SerializerMethodField to modify value of a particular field in the serializer. Here's an example:
The field that I wanted to modify, let us call it is_modifyable. This field is present on the Django Model as models.BooleanField and hence it was not present in the list of fields on serializer definition and simply mentioned in the class Meta: definition under within the serializer definition.
So here's how my code looked before:
# in models.py
# Model definition
class SomeModel(models.Model):
is_modifyable = models.BooleanField(default=True)
# in serializers.py
# Serializer definition
class SomeModelSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('is_modifyable',)
As a result of the above, the value for the field is_modifyable was always fetched on the basis of what the value was in the record of SomeModel object. However, for some testing purpose, I wanted the value of this field to be returned as False during the development phase, hence I modified the code to be as follows:
# in models.py
# Model definition (Left unchanged)
class SomeModel(models.Model):
is_modifyable = models.BooleanField(default=True)
# in serializers.py
# Serializer definition (This was updated)
class SomeModelSerializer(serializers.ModelSerializer):
# This line was added new
is_modifyable = serializers.SerializerMethodField(read_only=True)
class Meta:
model = SomeModel
fields = ('is_modifyable',)
# get_is_modifyable function was added new
def get_is_modifyable(self, obj) -> bool:
"""
Dummy method to always return False for test purpose
Returns: False
"""
return False
Once the above code was in, the API call always returned the value of serializer field is_modifyable as False.
I have developed an API using django-rest-framework.
I am using ModelSerializer to return data of a model.
models.py
class MetaTags(models.Model):
title = models.CharField(_('Title'), max_length=255, blank=True, null=True)
name = models.CharField(_('Name'), max_length=255, blank=True, null=True)
serializer.py
class MetaTagsSerializer(serializers.ModelSerializer):
class Meta:
model = MetaTags
response
{
"meta": {
"title": null,
"name": "XYZ"
}
}
Ideally in an API response any value which is not present should not be sent in the response.
When the title is null I want the response to be:
{
"meta": {
"name": "XYZ"
}
}
I found this solution to be the simplest.
from collections import OrderedDict
from rest_framework import serializers
class NonNullModelSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
result = super(NonNullModelSerializer, self).to_representation(instance)
return OrderedDict([(key, result[key]) for key in result if result[key] is not None])
I faced a similar problem and solved it as follows:
from operator import itemgetter
class MetaTagsSerializer(serializers.ModelSerializer):
class Meta:
model = MetaTags
def to_representation(self, instance):
ret = super().to_representation(instance)
# Here we filter the null values and creates a new dictionary
# We use OrderedDict like in original method
ret = OrderedDict(filter(itemgetter(1), ret.items()))
return ret
Or if you want to filter only empty fields you can replace the itemgetter(1) by the following:
lambda x: x[1] is not None
The answer from CubeRZ didn't work for me, using DRF 3.0.5. I think the method to_native has been removed and is now replaced by to_representation, defined in Serializer instead of BaseSerializer.
I used the class below with DRF 3.0.5, which is a copy of the method from Serializer with a slight modification.
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.fields import SkipField
class NonNullSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
if attribute is not None:
represenation = field.to_representation(attribute)
if represenation is None:
# Do not seralize empty objects
continue
if isinstance(represenation, list) and not represenation:
# Do not serialize empty lists
continue
ret[field.field_name] = represenation
return ret
EDIT incorporated code from comments
You could try overriding the to_native function:
class MetaTagsSerializer(serializers.ModelSerializer):
class Meta:
model = MetaTags
def to_native(self, obj):
"""
Serialize objects -> primitives.
"""
ret = self._dict_class()
ret.fields = self._dict_class()
for field_name, field in self.fields.items():
if field.read_only and obj is None:
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
# Continue if value is None so that it does not get serialized.
if value is None:
continue
method = getattr(self, 'transform_%s' % field_name, None)
if callable(method):
value = method(obj, value)
if not getattr(field, 'write_only', False):
ret[key] = value
ret.fields[key] = self.augment_field(field, field_name, key, value)
return ret
I basically copied the base to_native function from serializers.BaseSerializer and added a check for the value.
UPDATE:
As for DRF 3.0, to_native() was renamed to to_representation() and its implementation was changed a little. Here's the code for DRF 3.0 which ignores null and empty string values:
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
# KEY IS HERE:
if attribute in [None, '']:
continue
# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
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
Two Options are added with:
Remove key having value None
Remove key having value None or Blank.
from collections import OrderedDict
from rest_framework import serializers
# None field will be removed
class NonNullModelSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
result = super(NonNullModelSerializer, self).to_representation(instance)
return OrderedDict([(key, result[key]) for key in result if result[key] is not None])
# None & Blank field will be removed
class ValueBasedModelSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
result = super(ValueBasedModelSerializer, self).to_representation(instance)
return OrderedDict([(key, result[key]) for key in result if result[key] ])
Simply modification for none & blank value based key removal, for my
use. Thanks goes to #Simon.
If there a way to detect if information in a model is being added or changed.
If there is can this information be used to exclude fields.
Some pseudocode to illustrate what I'm talking about.
class SubSectionAdmin(admin.ModelAdmin):
if something.change_or_add = 'change':
exclude = ('field',)
...
Thanks
orwellian's answer will make the whole SubSectionAdmin singleton change its exclude property.
A way to ensure that fields are excluded on a per-request basis is to do something like:
class SubSectionAdmin(admin.ModelAdmin):
# ...
def get_form(self, request, obj=None, **kwargs):
"""Override the get_form and extend the 'exclude' keyword arg"""
if obj:
kwargs.update({
'exclude': getattr(kwargs, 'exclude', tuple()) + ('field',),
})
return super(SubSectionAdmin, self).get_form(request, obj, **kwargs)
which will just inform the Form to exclude those extra fields.
Not sure how this will behave given a required field being excluded...
Setting self.exclude does as #steve-pike mentions, make the whole SubSectionAdmin singleton change its exclude property.
A singleton is a class that will reuse the same instance every time the class is instantiated, so an instance is only created on the first use of the constructor, and subsequent use of the constructor will return the same instance. See the wiki page for a more indept description.
This means that if you write code to exclude the field on change it will have the implication that if you first add an item, the field will be there, but if you open an item for change, the field will be excluded for your following visits to the add page.
The simplest way to achieve a per request behaviour, is to use get_fields and test on the obj argument, which is None if we are adding an object, and an instance of an object if we are changing an object. The get_fields method is available from Django 1.7.
class SubSectionAdmin(admin.ModelAdmin):
def get_fields(self, request, obj=None):
fields = super(SubSectionAdmin, self).get_fields(request, obj)
if obj: # obj will be None on the add page, and something on change pages
fields.remove('field')
return fields
Update:
Please note that get_fields may return a tuple, so you may need to convert fields into a list to remove elements.
You may also encounter an error if the field name you try to remove is not in the list. Therefore it may, in some cases where you have other factors that exclude fields, be better to build a set of excludes and remove using a list comprehension:
class SubSectionAdmin(admin.ModelAdmin):
def get_fields(self, request, obj=None):
fields = list(super(SubSectionAdmin, self).get_fields(request, obj))
exclude_set = set()
if obj: # obj will be None on the add page, and something on change pages
exclude_set.add('field')
return [f for f in fields if f not in exclude_set]
Alternatively you can also make a deepcopy of the result in the get_fieldsets method, which in other use cases may give you access to better context for excluding stuff. Most obviously this will be useful if you need to act on the fieldset name. Also, this is the only way to go if you actually use fieldsets since that will omit the call to get_fields.
from copy import deepcopy
class SubSectionAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj=None):
"""Custom override to exclude fields"""
fieldsets = deepcopy(super(SubSectionAdmin, self).get_fieldsets(request, obj))
# Append excludes here instead of using self.exclude.
# When fieldsets are defined for the user admin, so self.exclude is ignored.
exclude = ()
if not request.user.is_superuser:
exclude += ('accepted_error_margin_alert', 'accepted_error_margin_warning')
# Iterate fieldsets
for fieldset in fieldsets:
fieldset_fields = fieldset[1]['fields']
# Remove excluded fields from the fieldset
for exclude_field in exclude:
if exclude_field in fieldset_fields:
fieldset_fields = tuple(field for field in fieldset_fields if field != exclude_field) # Filter
fieldset[1]['fields'] = fieldset_fields # Store new tuple
return fieldsets
class SubSectionAdmin(admin.ModelAdmin):
# ...
def change_view(self, request, object_id, extra_context=None):
self.exclude = ('field', )
return super(SubSectionAdmin, self).change_view(request, object_id, extra_context)
The approach below has the advantage of not overriding the object wide exclude property; instead it is reset based on each type of request
class SubSectionAdmin(admin.ModelAdmin):
add_exclude = ('field1', 'field2')
edit_exclude = ('field2',)
def add_view(self, *args, **kwargs):
self.exclude = getattr(self, 'add_exclude', ())
return super(SubSectionAdmin, self).add_view(*args, **kwargs)
def change_view(self, *args, **kwargs):
self.exclude = getattr(self, 'edit_exclude', ())
return super(SubSectionAdmin, self).change_view(*args, **kwargs)
I believe you can override get_fieldsets method of ModeAdmin class. See the example below, in the code example below, I only want to display country field in the form when adding a new country, In order to check if object is being added, we simply need to check if obj == None, I am specifying the fields I need. Now otherwise obj != None means existing object is being changed, so you can specify which fields you want to exclude from the change form.
def get_fieldsets(self, request: HttpRequest, obj=None):
fieldset = super().get_fieldsets(request, obj=obj)
if obj == None: # obj is None when you are adding new object.
fieldset[0][1]["fields"] = ["country"]
else:
fieldset[0][1]["fields"] = [
f.name
for f in self.model._meta.fields
if f.name not in ["id", "country"]
]
return fieldset
You can override the get_exclude method of the admin.ModelAdmin class:
def get_exclude(self, request, obj):
if "change" in request.path.split("/"):
return [
"fields",
"to",
"exclude",
]
return super().get_exclude(request, obj)
I think this is cleaner than the provided answers. It doesn't override the exclude field of the Class explicitly, but rather only contextually provides the fields you wish to exclude depending on what view you're on.