Django Rest Framework serializing with dynamic fields - django

I am having an issue with serialization. I have a queryset of objects e.g.:
uvs = UserVehicles.objects.all()
Some of those objects are expired, some are not. I would like to have different fields in serializer, depending on expiry information. For example, I would like to exclude status and distance_travelled fields from expired objects. What is the easiest way to achieve that? I tried with the next code, but self.object in init method contains an array, so it would remove fields for all objects, not just expired ones.
serialized_data = UserVehicleSerializer(uvs, many=True).data
class UserVehicleSerializer(serializers.ModelSerializer):
class Meta:
model = UserVehicle
fields = ('type', 'model', 'status', 'distance_travelled',)
def __init__(self, *args, **kwargs):
super(UserVehicleSerializer, self).__init__(*args, **kwargs)
if self.object.is_expired:
restricted = set(('distance_travelled', 'status',))
for field_name in restricted:
self.fields.pop(field_name)

I would suggest keeping the business logic out of the serializer. you could create a separate serializer for expired vehicles/objects and a separate serializer for active vehicles and choose the correct serializer in the view. That way, if your business logic goes in different directions for each type of vehicle , it should be easy to manage.

You could do that in the Serializer's to_representation().
http://www.django-rest-framework.org/api-guide/fields/#custom-fields has examples for Fields but Serializers do inherit from Field.
Just call the parent's to_representation and remove the fields you don't want.

Related

How to use the serializer in part

I have several viewsets, several endpoints in them use one serializer. One endpoint does not even have a Meta class,
It performs a certain action and uses the same serializer in the method to_representation. In this serializer I use the methodfield like this:
some_field = serializers.SerializerMethodField()
def get_some_field(self, obj):
return bool(obj.something_attr)
something_attr I get in viewset in
queryset =MyModel.objects.annotate(something_attr=(...))
In others viewsets there is no such field, so they use other queriesets. Can I work around this problem without creating a bunch of additional serializers. My thanks!
As per my understanding you're trying to use a single serializer class with different views - but each view need different fields, right?
class DynamicFieldSerializerMixin:
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
super().__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)
You can use this mixin to define fields dynamically for each view for a single serializer class. Just pass fields=[] keyword argument with list of field names.

Django Rest Framework, ModelSerializers and custom fields validation

Using latest version of Django and DRF.
I have a rather complex requirement I can't find a solution for. I'll try to simplify it.
Let's say I have a model that has two fields. field_a and field_b
I have a ModelSerializer for it. I POST a request with its fields. The fields get validated with the model and then against my two serializer functions validate_field_a and validate_field_b. All is well.
Now I'd like my POST request to include a third field that is not a member of that model. let's call it field_c. I have a custom def create(self, validated_data): in my serializer which saves everything to the database.
with regards to field_c I would like to:
Custom Validate it. just like I do with the other two fields.
Require that it is mandatory for the whole request to succeed and if it's not, issue a "Field is required" error just like if I forgot to POST one of my required model fields.
Have the chance to take field_c and save it onto a totally different unrelated Model's row in the db.
I can't seem to get around that. If I add field_c to the fields meta - it throws an exception saying justifiably that field_c is not in my model. If I don't include it in fields, the validate_field_c which I really want to put there doesn't even get called.
What can I do?
You can add the custom field in your serializer as a write_only field and override the create method so that you can handle the custom field's value.
Something like this:
class MySerializer(serializers.ModelSerializer):
field_c = serializers.CharField(write_only=True)
class Meta:
model = MyModel
fields = ('field_a', 'field_b', 'field_c')
def validate_field_c(self, value):
if value is 'test':
raise ValidationError('Invalid')
return value
def create(self, validated_data, **kwargs):
field_c = validated_data.pop('field_c')
return MyModel.objects.create(**validated_data)
Don't use ModelSerializer for this - use a serializer that recreates the same fields as your model & include field_c as you would.
I understand that you want your model to do some of the work in the validation process but the design of DRF is such that it isolates these responsibilities. You can read more about it here. Basically, the serializer should be the one doing all the validation heavy-lifting.
Of course, this means that you'll have to explicitly define the validation methods in the serializer.
In your custom create() method you can create the model instance or do whatever you want in it as required.

In Django REST framework 3, can one ModelSerializer have different sets of fields for create vs. update?

I have a DRF 3.3+ API to create/update/retrieve users. I use the super-convenient write_only serializer field argument on my password field so that it's used to create/update a user, but is not returned when serializing a user. However, I want to make password required to create a user, but optional to update a user. Instead of write_only, it would be great to have something like create_only and update_only for finer-grained control. Since that's not available, I have two serializers that are exactly the same except for the password field, which doesn't seem clean.
I'm aware of this answer for DRF 2: Disable field update after object is created, but I was hoping there's a better way to handle this use case in DRF 3.3+. Thanks in advance for any insight.
There's no create_only or update_only like options.
You could override __init__ to see if the instance parameter was passed and adjust fields accordingly.
I'd have two serialisers, just as you do. So as not to repeat too much, I have one subclass the other, with the subclass adjusting only the fields that differed.
I think there is no inbuilt functionality for that.
But you could do something like this to define the fields depening on the action:
class FooSerializer(serializers.ModelSerializer):
# define fields depending on action
_action_fields = {'update': ['name'],
'create': ['name', 'password'],
'default': ['name', 'password']}
class Meta:
model = Foo
fields = ['name', 'password'] # define max fields you want serialize
def __init__(self, *args, **kwargs):
serializers.ModelSerializer.__init__(self, *args, **kwargs)
action = kwargs['context']['view'].get('action', 'default') # I'm not 100% sure if action is defined here. But something similar
allowed = set(self._action_fields[action])
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)

Django ModelForm- How to make a form generated uneditable

I am learning django form and want to know how to make a model form generated display only.
models.py
class Person(models.Model):
first_name = models.CharField(max_length=40, null=True)
last_name = models.CharField(max_length=40, null=True)
#more fields
forms.py
class PersonForm(ModelForm):
class Meta:
model = Person
To generate a form with some existing data in the database:
person=Person.objects.get(id=someid)
person_form = PersonForm(instance = person)
All the fields in the form are editable in the page. However, I just want to display the data.
After some searching in StackOverflow I found a similar solution how to show a django ModelForm field as uneditable , which teaches how to set individual field uneidtable.
But I want to make the whole form uneditable. Is there any better way to do so instead of setting all the fields as uneditable one by one?
Thank you very much for your help.
Updates: I find the flowing code helps make the form uneditable, but still not sure whether this is the correct way to do it.
for field in person_form.fields:
person_form.fields[field].widget.attrs['readonly'] = True
Thank you for giving your advice.
There is no attribute called editable or something similar on the form which can act on all the fields. So, you can't do this at form level.
Also, there is no such attribute on Field class used by django forms as well, so it wouldn't be possible to set such attribute and make the field read only. So, you will have to operate on on the fields of the form in __init__ of your form.
class PersonForm(ModelForm):
class Meta:
model = Person
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
for name, field in self.fields.iteritems():
field.widget.attrs['readonly'] = 'true'
In case, you only want to make some fields uneditable, change the __init__.
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
uneditable_fields = ['first_name', 'last_name']
for field in uneditable_fields:
self.fields[field].widget.attrs['readonly'] = 'true'
Another solution perhaps, do not have to do any processing, just display like this..
<table border='1'>
{% for field in form%}
<tr>
<td>{{field.label}}</td>
<td>{{field.value}}</td>
</tr>
{% endfor%}
</table>
I know, old question, but since I had the same question this week it might help other people.
This technique only works if you want the whole form to be readonly. It overrides any posted data (see def clean(self)) and sets the widget attributes to readonly.
Note: Setting the widget attributes to readonly does not prevent altering the model object instance.
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.is_readonly():
for k,f in self.fields.iteritems():
f.widget.attrs['readonly'] = True
def clean(self):
if self.is_readonly():
return {}
return super(CompanyQuestionUpdateForm, self).clean()
def is_readonly(self, question):
if your_condition:
return True
return False
class Meta:
model = MyModel
It is possible to implement field widget to render bound ModelForm field values wrapped into div or td, sample implementation is there
https://github.com/Dmitri-Sintsov/django-jinja-knockout/blob/master/django_jinja_knockout/widgets.py
# Read-only widget for existing models.
class DisplayText(Widget):
Then a form metaclass can be implemented which will set field widget to DisplayText for all ModelForm fields automatically like that:
https://github.com/Dmitri-Sintsov/djk-sample/search?utf8=%E2%9C%93&q=DisplayModelMetaclass
class ClubDisplayForm(BootstrapModelForm, metaclass=DisplayModelMetaclass):
class Meta(ClubForm.Meta):
widgets = {
'category': DisplayText()
}
Feel free to use or to develop your own versions of widget / form metaclass.
There was discussion about read-only ModelForms at django bug ticket:
https://code.djangoproject.com/ticket/17031
closed as "Froms are for processing data, not rendering it."
But I believe that is mistake for these reasons:
ModelForms are not just processing data, they also map forms to models. Read-only mapping is the subset of mapping.
There are inline formsets and having read-only inline formsets is even more convenient, it leaves a lot of burden from rendering relations manually.
Class-based views can share common templates to display and to edit ModelForms. Thus read-only display ModelForms increase DRY (one of the key Django principles).

Can I create a custom django modelchoicefield with a default queryset

I have an order model with a followed_by field:
class order(models.Model):
followed_by = models.ForeignKey(User, limit_choices_to={'groups__name': "Managers"})
I have several such models and forms for those models. By default the form displays a modelchoicefield listing users that are mangers. This is fine. But the display isn't nice: it gives the username, and I want first+last name. This would work nicely: Change Django ModelChoiceField to show users' full names rather than usernames
except that now in everyform I must declare the queryset to limit users to managers. Can I use the above method so that the custom modelchoicefield defaults to my filtered queryset. so then from a form I can just say:
followed_by = ManagerUserModelChoiceField()
Can you define the queryset on your ModelChoiceField child class?
class UserModelChoiceField(ModelChoiceField):
# Query that returns set of valid choices
queryset = User.objects.filter(group__name='Managers')
def label_from_instance(self, obj):
return obj.get_full_name()
Try passing in the queryset as an argument to the ManagerUserModelChoiceField class.
followed_by = ModelChoiceField(queryset = User.objects.filter(groups__name="Managers")
After my comment to #Enrico this thought occurred to me: I overwrote the "init" class on my custom field like so:
class UserModelChoiceField(forms.ModelChoiceField):
def __init__(self, *args, **kwargs):
super(UserModelChoiceField, self).__init__(queryset=User.objects.filter(groups__name="Managers"), *args, **kwargs)
I've seen stuff like this done in python before but I'm new to python so I'm not sure if this is a bad thing to do or if I should make this better somehow? I'd appreciate some feedback. That being said, it seems to be working correctly.