I'm trying to write a Serializer that would take dynamic fields and add them to the restricted number of fields specified in Meta, but it seems that there's no method to "add back" a field to the serializer once it's been created.
Dynamic Fields per Django documentation
class DynamicFieldsModelSerializer(ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class BldgLocationAndFilters(DynamicFieldsModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = ('latitude', 'longitude')
I'd like to do something that would modify the DynamicFieldsModelSerializer such that fields can be appended to the set that has already been filtered down, but it looks like the Meta fields override everything such that nothing can be added back (fields can only be removed
Pseudocode of desired behavior:
class DynamicFieldsUnionModelSerializer(ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
new_fields = set(fields)
existing = set(self.fields)
unique_new = new_fields.union(existing) - existing
for field_name in unique_new:
self.fields.update(field_name)
If BldgLocationAndFilters was called as serializer = BldLocationAndFilters(fields=['type']), I'd expect the resulting returns to have fields = ('latitude', 'longitude', 'type')
DynamicFieldsModelSerializer only works on removing existing fields, because the implementation depends on fields already built in __init__. You can add fields after the __init__, but you have to build them again somehow (not just add names).
But one way to support this is to override the serializer's get_field_names method which works with unbuilt field names:
class BldgLocationAndFilters(ModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = ('latitude', 'longitude')
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
self._fields_to_add = kwargs.pop('fields', None)
super().__init__(*args, **kwargs)
def get_field_names(self, *args, **kwargs):
original_fields = super().get_field_names(*args, **kwargs)
if self._fields_to_add:
return set(list(original_fields) + list(self._fields_to_add))
return original_fields
# Should use latitude, longitude, and type
BldgLocationAndFilters(instance, fields=('type',)).data
Note that this is using just ModelSerializer.
Or just define your serializer with __all__ (while still using DynamicFieldsModelSerializer) and set the fields on a per-need basis:
class BldgLocationAndFilters(DynamicFieldsModelSerializer):
latitude = fields.FloatField(source='lat_016_decimal')
longitude = fields.FloatField(source='long_017_decimal')
class Meta:
model = Bldg
fields = '__all__'
BldgLocationAndFilters(instance, fields=('latitude', 'longitude', 'type')).data
Related
I have a table with a blob and would like to exclude it from being in the sql call to the database unless specifically called for. Out of the box django includes everything in the queryset. So far the only way I have found to limit the field is to add a function to the view get_queryset()
def filter_queryset_fields(request, query_model):
fields = request.query_params.get('fields')
if fields:
fields = fields.split(',')
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set([f.name for f in query_model._meta.get_fields()])
values = []
for field_name in existing & allowed:
values.append(field_name)
queryset = query_model.objects.values(*values)
else:
queryset = query_model.objects.all()
return queryset
class TestViewSet(DynamicFieldsMixin, viewsets.ReadOnlyModelViewSet):
queryset = models.TestData.objects.all()
serializer_class = serializers.TestSerializer
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
filter_fields = ('id', 'frame_id', 'data_type')
def get_queryset(self):
return filter_queryset_fields(self.request, models.TestData)
and mixin to the serializer to limit the fields it checks
class DynamicFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DynamicFieldsMixin, self).__init__(*args, **kwargs)
if "request" in self.context and self.context['request'] is not None:
fields = self.context['request'].query_params.get('fields')
if fields:
fields = fields.split(',')
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class TestSerializer(DynamicFieldsMixin, rest_serializers.ModelSerializer):
class Meta:
model = models.TestData
fields = '__all__'
this seems like a lot of code for what it does. Is there an easier way?
Django already have this right out the box. Just use defer or only
Defer will allow you the exclude a set of fields from the queryset:
MyModel.objects.defer('field_i_want_to_exclude')
While only allows you to say what fields you want on the queryset:
MyModel.objects.only('field_i_want1', 'field_i_want2')
Id do something like this for single, NON nested objects. For nested properties, you'll need more logic.
class DynamicFieldListSerializer(serializers.ListSerializer):
def to_representation(self, data):
"""
Code is a copy of the original, with a modification between the
iterable and the for-loop.
"""
# Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed
iterable = data.all() if isinstance(data, models.Manager) else data
fields = list(self.child.get_fields().keys())
iterable = iterable.only(*fields)
return [
self.child.to_representation(item) for item in iterable
]
class DynamicSerializerFieldsMixin:
def get_fields(self):
fields = super().get_fields()
raw_fields = set(self.context['request'].GET.get('fields', '').split(','))
# If querysparams ?fields= doesn't evaluate to anything, default to original
validated_fields = set(raw_fields) & set(fields.keys()) or set(fields.keys())
return {key: value for key, value in fields.items() if key in validated_fields}
#classmethod
def many_init(cls, *args, **kwargs):
meta = getattr(cls, 'Meta', None)
if not hasattr(meta, 'list_serializer_class'):
meta.list_serializer_class = DynamicFieldListSerializer
return super().many_init(*args, **kwargs)
For examples see:
https://gist.github.com/kingbuzzman/d7859d9734b590e52fad787d19c34b52#file-django_field_limit-py-L207
Use values():
MyModel.objects.values('column1', 'column2')
I am trying to set the initial value of a field on a form. The field is not part of the model, but when I try and set it to a value the field is blank. From my research it could be because the form is "bound" which makes some sense to me, but in this case the field is not part of the model.
My form:
#Form for editing profile
class CatForm(forms.ModelForm):
pictureid = forms.CharField()
class Meta:
model = Cat
fields = ['name']
def __init__(self, *args, **kwargs):
picid = kwargs.pop("pictureid")
print(picid)
super(CatForm, self).__init__(*args, **kwargs)
self.fields['pictureid'] = forms.CharField(initial=picid, required=False)
The model:
class Cat(models.Model):
name = models.CharField(max_length=34,null=False)
From the view it is called like this:
catform = CatForm(request.POST, pictureid=instance.id)
I was expecting it to set the field to the value of the initial attribute, but it doesn't. I have tried testing it by directly adding a string, but doesn't set.
This is what seems to be working for me:
class CatForm(forms.ModelForm):
class Meta:
model = Cat
fields = ['name']
def __init__(self, *args, **kwargs):
picid = kwargs.pop("pictureid")
super(CatForm, self).__init__(*args, **kwargs)
self.fields['pictureid'] = forms.CharField(initial=picid)
I also needed to drop the "request.POST" from the call to this when initiating the form.
If you want to render the pictureid in GET request, then you can try like this:
catform = CatForm(initial={'pictureid': instance.id})
For GET request, you don't need to override the __init__ method.
But, if you want to use the Catform in POST request, to use the value of pictureid somewhere else(lets say in save method), then you will need to override __init__ method here.
class CatForm(forms.ModelForm):
pictureid = forms.CharField()
class Meta:
model = Cat
fields = ['name']
def __init__(self, *args, **kwargs):
picid = kwargs.pop("pictureid")
print(picid)
super(CatForm, self).__init__(*args, **kwargs)
self.pictureid = picid
def save(self, *args, **kwargs):
print(self.pictureid) # if you want to use it in save method
return super().save(*args, **kwargs)
class MyCustomInline(admin.TabularInline):
min_num = 1
extra = 0
fields = ['matcher', 'param0', 'param1']
model = MyModel
form = MyCustomInlineForm
def get_formset(self, request, obj=None, **kwargs):
extra_fields = {
'param0': forms.CharField(label='First Param', required=False),
'param1': forms.CharField(label='Second Param', required=False)
}
kwargs['form'] = type('MyCustomInline', (MyCustomInlineForm,), extra_fields)
return super(MyCustomInline, self).get_formset(request, obj, **kwargs)
This is basically how I define my inline form so that it has two extra fields - matcher is a standard field in the related table and the inline form handles it automatically. And I save the extra param values in a different storage via overriding the save() in MyCustomInlineForm.
But if I edit an existing record - matcher value appears correctly but I obviously also want to preload the param0 and param1 with the corresponding values. Where can I hook up to do that?
I managed to do it on my own. I also managed to simplify the way I define my custom extra fields, without overriding get_formset method:
class MyCustomInlineForm(forms.ModelForm):
matcher = forms.ChoiceField(choices=[(v['name'], v['name']) for v in matchers], label='Matcher')
param0 = forms.CharField(label='First Param', required=False)
param1 = forms.CharField(label='Second Param', required=False)
def __init__(self, *args, **kwargs):
super(MyCustomInlineForm, self).__init__(*args, **kwargs)
if self.instance.pk:
""" self.instance is the model for the current row.
If there is a pk property that is not None, it means it's not
a new, empty inline model but we are working with existing one."""
self.initial['param0'], self.initial['param1'] = custom_way_to_load_params(self.instance)
def save(self, commit=True):
model = super(MyCustomInlineForm, self).save(True)
param0 = self.cleaned_data['param0']
param1 = self.cleaned_data['param1']
custom_way_to_save_params(model, param0, param1)
return model
class MyCustomInline(admin.TabularInline):
min_num = 1
extra = 0
fields = ['matcher', 'param0', 'param1']
model = MyModel
form = MyCustomInlineForm
If needed - validation of custom params could be done by overriding is_valid() method of forms.ModelForm class and adding errors via self.add_error(). I hope it helps someone.
I'm trying to populate a Select list of a ModelForm, with the Django groups the current users belongs to.
No errors arise, but I get only an empty Select list.
This is my code:
class ArchiveForm(forms.ModelForm):
class Meta:
model = Archive
fields = ['tags', 'version', 'sharegp']
localized_fields = None
labels = {'tags': 'Related Keywords'}
sharegp = forms.ChoiceField(label='Share with groups')
def __init__(self, user, *args, **kwargs):
#import pudb;pudb.set_trace()
self.user = user
super(ArchiveForm, self).__init__(*args, **kwargs)
self.fields['sharegp'].queryset = Group.objects.filter(user=self.user)
self.fields['sharegp'].widget.choices = self.fields['sharegp'].choices
Note that if I enable the debugger in the first line of the __init__ method, and step forward all along the function, the line:
self.fields['sharegp'].queryset
Gives the correct list containing the groups for that user, but that is not passed to the actual form.
What could I be missing? Thank you!
This is how I ended up solving this:
I was wrongly choosing the type of the field: The correct one is ModelChoiceField:
class ArchiveForm(forms.ModelForm):
class Meta:
model = Archive
fields = ['tags', 'version', 'sharegp']
localized_fields = None
labels = {'tags': 'Related Keywords'}
user = None
usergroups = None
sharegp = forms.ModelChoiceField(label='Share with groups', queryset=usergroups)
def __init__(self, user, *args, **kwargs):
self.user = user
self.usergroups = Group.objects.filter(user=self.user)
super(ArchiveForm, self).__init__(*args, **kwargs)
self.fields['sharegp'].queryset = self.usergroups
That last line is overwriting the queryset assigned in previous one. Remove it.
I have a serializer in Django REST framework defined as follows:
class QuestionSerializer(serializers.Serializer):
id = serializers.CharField()
question_text = QuestionTextSerializer()
topic = TopicSerializer()
Now I have two API views that use the above serializer:
class QuestionWithTopicView(generics.RetrieveAPIView):
# I wish to include all three fields - id, question_text
# and topic in this API.
serializer_class = QuestionSerializer
class QuestionWithoutTopicView(generics.RetrieveAPIView):
# I want to exclude topic in this API.
serializer_class = ExamHistorySerializer
One solution is to write two different serializers. But there must be a easier solution to conditionally exclude a field from a given serializer.
Have you tried this technique
class QuestionSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
remove_fields = kwargs.pop('remove_fields', None)
super(QuestionSerializer, self).__init__(*args, **kwargs)
if remove_fields:
# for multiple fields in a list
for field_name in remove_fields:
self.fields.pop(field_name)
class QuestionWithoutTopicView(generics.RetrieveAPIView):
serializer_class = QuestionSerializer(remove_fields=['field_to_remove1' 'field_to_remove2'])
If not, once try it.
Creating a new serializer is the way to go. By conditionally removing fields in a serializer you are adding extra complexity and making you code harder to quickly diagnose. You should try to avoid mixing the responsibilities of a single class.
Following basic object oriented design principles is the way to go.
QuestionWithTopicView is a QuestionWithoutTopicView but with an additional field.
class QuestionSerializer(serializers.Serializer):
id = serializers.CharField()
question_text = QuestionTextSerializer()
topic = TopicSerializer()
class TopicQuestionSerializer(QuestionSerializer):
topic = TopicSerializer()
You can set fields and exclude properties of Meta
Here is an Example:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ['id', 'email', 'mobile']
def __init__(self, *args, **kwargs):
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
# #note: For example based on user, we will send different fields
if self.context['request'].user == self.instance.user:
# Or set self.Meta.fields = ['first_name', 'last_name', 'email', 'mobile',]
self.Meta.exclude = ['id']
Extending above answer to a more generic one
class QuestionSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
super(QuestionSerializer, self).__init__(*args, **kwargs)
if fields is not None:
allowed = set(fields.split(','))
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class QuestionWithoutTopicView(generics.RetrieveAPIView):
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
fields = self.request.GET.get('display')
serializer_class = self.get_serializer_class()
return serializer_class(fields=fields,*args, **kwargs)
def get_serializer_class(self):
return QuestionSerializer
Now we can give a query parameter called display to output any custom display format http://localhost:8000/questions?display=param1,param2
You can use to representation method and just pop values:
def to_representation(self, instance):
"""Convert `username` to lowercase."""
ret = super().to_representation(instance)
ret.pop('username') = ret['username'].lower()
return ret
you can find them here
https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior