Let's say I have a form such as:
from django import forms
class Foo(forms.ModelForm):
class Meta:
model = SomeModel
fields = ('field1', 'field2', 'field3')
How can I include or exclude fields based on some condition?
Edit:
My bad, I should have clarified that the condition needs to be made based on a value on each model that is being rendered. That means that I need to add (or remove) fields based on the values of the model that is currently being rendered.
You can modify form fields via __init__:
from django import forms
class Foo(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if condition:
self.fields.pop('field1')
class Meta:
model = SomeModel
fields = ('field1', 'field2', 'field3')
Note that this can cause the form's validation to fail if the field is required.
A better approach might be to hide the field, instead of removing it entirely:
class Foo(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if condition:
form.fields['field1'].widget = forms.HiddenInput()
class Meta:
model = SomeModel
fields = ('field1', 'field2', 'field3')
Related
I need to pass fields that are present in serializer, but not present in model to model save method (I have complicated saving logic and I want to make some decisions in object creation based on these fields). How can I do that? I tried to add
non_db_field = property to model, but I still get error MyModel() got an unexpected keyword argument 'negative_amount'
Let's say my model is
class MyModel(AbstractModel):
field1 = models.DateTimeField()
field2 = models.BigIntegerField()
My serializer is
class MyModelSerializer(AbstractSerializer):
field3 = serializers.BooleanField(required=False)
class Meta(AbstractSerializer.Meta):
model = MyModel
fields = '__all__'
And my viewset is
class MyModelViewSet(AbstractViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You should handle this behavior in serializer.save method, for example, you can pop it from validated_data like that:
def save(self, **kwargs):
self.validated_data.pop("negative_amount")
return super().save(**kwargs)
You can use fields=['field1', 'field2', 'field3'] in serializer instead of fields='__all__'.
I found a solution based partly on Sharpek's answer and partly based on this answer:
In serializer I override save method:
def save(self, **kwargs):
if 'field3' in self.validated_data:
kwargs['field3'] = self.validated_data.pop('field3')
return super().save(**kwargs)
In models I override init method and define field:
field3 = None
def __init__(self, *args, **kwargs):
if 'field3' in kwargs:
self.field3 = kwargs.pop('field3')
super(Reading, self).__init__(*args, **kwargs)
I would like to build a form with dynamically fields depends on needs and i have tried this code but doesn't work, the model form show all fields.
forms.py:
class CustomModelForm(forms.ModelForm):
class Meta:
model = app_models.CustomModel
fields = '__all__'
def __init__(self, excluded_fields=None, *args, **kwargs):
super(CustomModelForm, self).__init__(*args, **kwargs)
for meta_field in self.fields:
if meta_field in excluded_fields:
# None of this instructions works
-> del self.fields[meta_field]
-> self.fields.pop(meta_field)
-> self.fields.remove(meta_field)
Anybody could help me ?
Thanks in advance.
Alternatively, could you use the modelform_factory?
from django.forms import modelform_factory
CustomModelForm = modelform_factory(MyModel, exclude=('field_1', 'field_2'))
That way you could determine the exclude fields before creating the form, and just pass them into the factory, no need to override the constructor then.
The problem was the list of excluded_fields came from model._meta.get_fields(), not is a list of strings, and the if condition didnt matched well because self.fields is a python ordereddict.
This code solve the problem:
class CustomModelForm(forms.ModelForm):
class Meta:
model = app_models.CustomModel
fields = '__all__'
def __init__(self, excluded_fields=None, *args, **kwargs):
super(CustomModelForm, self).__init__(*args, **kwargs)
show_fields = []
for field in excluded_fields:
show_fields.append(field.name)
for meta_field in list(self.fields):
if meta_field not in show_fields:
del self.fields[meta_field]
I am creating my form in Form.py like this:
class pdftabelModelForm(forms.ModelForm):
class Meta:
model = pdftabel_tool_
fields = ['apn', 'owner_name']
apn = forms.ModelChoiceField(queryset= Field.objects.values_list('name', flat=True), empty_label="(Choose field)")
owner_name = forms.ModelChoiceField(queryset= Field.objects.values_list('name', flat=True), empty_label="(Choose field)")
But due to some reasons like 'self' is not available in form.py. I can only access it in views.py. So I want to make it like
class FieldForm(ModelForm):
class Meta:
model = pdftabel_tool_
fields = (
'apn',
'owner_name',)
How can I make these fields as dropdown like I did in my forms.py?
Why are you set on doing it in views.py? forms.py is the appropriate place to do this.
Instead of redefining your fields, you should use the form's __init__ method to override the querysets for your fields, like so:
class pdftabelModelForm(forms.ModelForm):
class Meta:
model = pdftabel_tool_
fields = ['apn', 'owner_name']
def __init__(self, *args, **kwargs):
super(pdftabelModelForm, self).__init__(*args, **kwargs)
self.fields['apn'].queryset = X
self.fields['owner_name'].queryset = X
EDIT: if you need to pass extra parameters to your form, update the init method to this:
def __init__(self, *args, **kwargs):
self.layer_id = self.kwargs.pop('layer_id')
super(pdftabelModelForm, self).__init__(*args, **kwargs)
self.fields['apn'].queryset = X
self.fields['owner_name'].queryset = X
And when you initialize your form from views.py, pass the parameter:
form = pdftableModelForm(layer_id=X)
I want to have a total read only ModelSerializer, i.e. just list/retrieve methods
what is the best way to do it?
You really want to do this at the view (or Viewset) level, which you can do with a ReadOnlyModelViewSet.
(You mentioned this in your comment but I'm leaving it as an answer for better visibility).
For example (from the documentation):
from rest_framework import viewsets
class AccountViewSet(viewsets.ReadOnlyModelViewSet):
"""
A simple ViewSet for viewing accounts.
"""
queryset = Account.objects.all()
serializer_class = AccountSerializer
If you do need a serializer to be read only, it's most concise and stable option to override the init method:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
setattr(self.Meta, 'read_only_fields', [*self.fields])
In comparison with the above:
#mgalgs's solution is best, if a view irrespective of the serializer used should be read only.
#thisisms solution is best if you only have a few and non-dynamic fields
#D W's approach will only work if there are no writable serializer fields that use the 'source' keyword.
EDIT better solution:
You can update the def get_fields method instead of the init method and create an abstract serializer:
class ReadOnlyModelSerializer(serializers.ModelSerializer):
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
for field in fields:
fields[field].read_only = True
return fields
To use it, just inherit from the abstract serializer:
def MySerializer(ReadOnlyModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
If you're using a ModelSerializer you can achieve this with the following:
from rest_framework import serializers
class MyModelSerializer(serializers.ModelSerializer):
...
class Meta:
model = MyModel
fields = ("field_1", "field_2", etc)
read_only_fields = [f.name for f in MyModel._meta.get_fields()]
This solution uses the private _meta interface of the django model, but it's fairly stable and it's used fairly extensively by developers.
Here we're simply generating a list of all the fields in the relevant model and applying it to the read_only_fields option from django-rest-framework.
I prefer doing this at the serializer level as opposed to the view level as it doesn't tie you down to using Viewset's as suggested by mgalgs.
If you wanted to take this a step further, you could even wrap the functionality into a mixin which adds to the Meta options of your class. Something such as:
from rest_framework import serializers
from rest_framework.fields import Field
class ReadOnlyMixin(Field):
def __new__(cls, *args, **kwargs):
setattr(
cls.Meta,
"read_only_fields",
[f.name for f in cls.Meta.model._meta.get_fields()],
)
return super(ReadOnlyMixin, cls).__new__(cls, *args, **kwargs)
class MyModelSerializer(ReadOnlyMixin, serializers.ModelSerializer):
...
class Meta:
model = MyModel
fields = ("field_1", "field_2", etc)
The only thing you have to do is create a serializer like this.
serializers.py
class YourdataSerializer(serializers.ModelSerializer):
class Meta:
model = Yourdata
# some data
fields = ('id', 'city', 'pincode', 'created')
read_only_fields = ('id', 'city', 'pincode', 'created')
Views something like this
class YourdataList(APIView):
def get(self, request, format=None):
yourdata = YourdataList.objects.all()
serializer = YourdataSerializer(yourdata, many=True)
return Response(serializer.data)
detail view
class YourdataDetail(APIView):
def get_object(self, pk):
try:
return Yourdata.objects.get(pk=pk)
except Yourdata.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = YourdataSerializer(snippet)
return Response(serializer.data)
This will do it.
Ok dear Mohsen,To implement a read-only serializer using the BaseSerializer class, we just need to override the .to_representation()method. Let's take a look at an example using a simple Django model:
class HighScore(models.Model):
created = models.DateTimeField(auto_now_add=True)
player_name=models.CharField(max_length=10)
score = models.IntegerField()
It's simple to create a read-only serializer for converting HighScore instances into primitive data types.
class HighScoreSerializer(serializers.BaseSerializer):
def to_representation(self, obj):
return { 'score': obj.score, 'player_name': obj.player_name }
two solutions for this?
from rest_framework.utils import model_meta
class CustomSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model = getattr(self.Meta, 'model')
declared_fields = copy.deepcopy(self._declared_fields)
info = model_meta.get_field_info(model)
field_names = self.get_field_names(declared_fields, info)
_, hidden_fields = self.get_uniqueness_extra_kwargs(
field_names, declared_fields, extra_kwargs)
fields_names.extend(hidden_fields.keys())
setattr(self.Meta, 'read_only_fields', list(field_names))
or
class CustomSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__()
setattr(self.Meta, 'read_only_fields', list(self.fields))
I have a model field
is_anonymous = BooleanField(default=False)
I also have a ModelForm. I want this field to be represented with a select widget.
It would be
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['is_anonymous']
widgets = {
'is_anonymous': forms.NullBooleanSelect(),
}
It works. But I want the select widget to only have two choices true (1) and false (0) and the represented text in each option should be Anonymous for true and self.request.user for false.
I think I have to do this replacement in the views as self.request.user is not available in the ModelForm.
How can I do this?
It's not 100% clear what you want, but if you want to display a select dropdown with only two choices; "Anonymous" which maps to True and "myusername" (i.e. the username of the current user) which maps to False, you need to override the is_anonymous field's widget's choices attribute:
class MyModelForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['is_anonymous'].widget = forms.Select(choices=[
(True, "Anonymous"),
(False, user.username)
])
class Meta:
model = MyModel
fields = ['is_anonymous']
because we need the user object in our form, we need to pass it manually as a parameter when defining the form in our view. This depends on the type of view you are using, but assuming it's a generic class based CreateView, you need the following:
class MyCreateView(CreateView):
form_class = MyModelForm
...
def get_form(self, form_class):
return self.form_class(self.request.user, **self.get_form_kwargs())
...