Django forms dynamic generation and validation - django

Let's say I have the following model
class Foo(models.Model):
title = models.CharField(default='untitled')
# Bar is a MPTT class, so I'm building a tree here
# Should not matter for my question...
root = models.ForeignKey(Bar)
leaf = models.ForeignKey(Bar)
To create new Foo objects I want to make use of a ModelForm like this:
class FooForm(ModelForm):
# possibly custom validation functions needed here...
class Meta:
model = Foo
fields = '__all__'
My view looks like this:
def create(request, leaf_id=None):
form = FooForm(data=request.POST or None)
if form.is_valid():
new = form.save()
return redirect('show.html', root_id=new.root.id)
return render('create_foo.html',
{ 'form': form })
As you can see, the view function should be used to handle two different use-cases:
/foo/create/
/foo/create/4where 4 is a leaf-ID.
If the leaf-ID is given, the form obviously isn't required to show a form field for this. Furthermore, root can be determined from leaf, so it isn't required aswell.
I know that I can dynamically change the used widgets, so I can switch them to HiddenInput, but I would like to not even show them as hidden to the user. But if I dynamically exclude them, they are not available for form validation and the whole process will fail during the validation process.
What I would like to achieve is: Show only form fields to the user, that are not yet pre-filled. Is there any best-practice available for this case?

You can do that by overriding the __init__() method of FooForm.
We override the __init__() method and check if instance argument was passed to the form. If instance was passed, we disable the root and leaf form fields so that it is not displayed in the template.
We will pass instance argument to the form when the request is of type foo/create/4 i.e. leaf_id is not None.
forms.py
class FooForm(ModelForm):
def __init__(self, *args, **kwargs):
super(FooForm, self).__init__(*args, **kwargs) # call the 'super()' init method
instance = getattr(self, 'instance', None) # get the `instance` form attribute
if instance and instance.id: # check if form has 'instance' attribute set and 'instance' has an id
self.fields['root'].widget.attrs['disabled'] = 'disabled' # disable the 'root' form field
self.fields['leaf'].widget.attrs['disabled'] = 'disabled' # disable the 'leaf' form field
# custom validation functions here
....
class Meta:
model = Foo
fields = '__all__'
In our view, we first check if leaf_id argument was passed to this view. If leaf_id was passed,we retrieve the Foo object having leaf id as the leaf_id. This instance is then passed when initializing a form and is updated when form.save() is called. We will use the instance to populate the form with values as the attributes set on the instance.
If leaf_id is not passed, then we initialize FooForm with data argument.
views.py
def create(request, leaf_id=None):
# Get the instance if any
instance = None
if leaf_id:
instance = Foo.objects.get(leaf_id=leaf_id) # get the 'Foo' instance from leaf_id
# POST request handling
if request.method=='POST':
if instance:
form = FooForm(data=request.POST, instance=instance) # Populate the form with initial data and supply the 'instance' to be used in 'form.save()'
else:
form = FooForm(data=request.POST)
if form.is_valid():
new = form.save()
return redirect('show.html', root_id=new.root.id)
return render('create_foo.html',
{ 'form': form })
# GET request handling
if instance:
form = FooForm(initial=instance._data, instance=instance) # form will be populated with instance data
else:
form = FooForm() # blank form is initialized
return render('create_foo.html',
{ 'form': form })

Related

Django modelForm update certain fields

I have a 'Farm' model and a corresponding ModelForm as follows:
class FarmForm(ModelForm):
class Meta:
model = Farm
fields = ['farm_name','address','farm_size', 'latitude', 'longitude']
I can save a new Farm object through my client app (it requires that I fill in all the fields mentioned in my ModelForm).
I want to have another view where in I can update an existing Farm where the user can perhaps insert/update only those fields he/she wants to change. I tried something like following by passing only one of the field values through Postman but it gives me Form_not_valid error:
#api_view(['POST'])
def updateFarm(request, farmId):
farm = Farm.objects.get(id=farmId)
form = FarmForm(instance=farm, data=request.POST)
if form.is_valid():
farm = form.save()
farm = Farm.objects.filter(id=farm.id)
serializer = FarmSerializer(farm, many=True)
return JSONResponse(serializer.data)
#return Response("Data saved")
else:
return Response("Form not valid, insert correct fields.")
How can I build my view that let's user update only those fields he thinks are relevant? My url: url(r'^farms/update/(?P<farmId>\d\d)/$', views.updateFarm),
You can generate a boolean hidden form field for every field in your model, that gets set when a field is modified. For example name input:
<input id="id_name" maxlength="100" name="name" type="text">
will be followed by a name__specified hidden input:
<input id="id_name__specified" name="name__specified" type="hidden">
You track changes to field name with some js (very easy with plain js or jquery) and update name__specified accordingly to true/false.
In order to do this automatically and be able to re-use it, you can abstract this in a base form class and keep your form simple:
class BaseForm(forms.ModelForm):
suffix = '__specified'
def __init__(self, **kwargs):
super(BaseForm, self).__init__(**kwargs)
fields = list(self.fields)
for f in fields:
# Set the field default value from the instance
self.fields[f].widget.attrs['default'] = getattr(self.instance, f)
# JS tracking field changes
js = """
document.getElementById("id_%s").value =
this.value != this.getAttribute("default");
""" % (f + self.suffix)
self.fields[f].widget.attrs['onchange'] = js
self.fields[f + self.suffix] = forms.BooleanField(
widget=forms.HiddenInput(),
required=False
)
def clean(self):
data = super(BaseForm, self).clean()
flags = [f for f in self.fields if self.suffix in f]
for x in flags:
specified = data.get(x, False)
if not specified:
field = x[:-len(self.suffix)]
# If not specified grab it's current value from the instance
data[field] = getattr(self.instance, field)
# If the form validation complains that it's missing
# clear the error since we are not changing it's value
if field in self.errors:
del self.errors[field]
return data
So your modified form:
class FarmForm(BaseForm):
class Meta:
model = Farm
fields = ['farm_name','address','farm_size', 'latitude', 'longitude']
Note, you should pass the instance when instantiating a form in your GET function or simply inherit your view from UpdateView so that will be handled automatically:
class MyView(UpdateView):
template_name = 'my_template.html'
form_class = FarmForm
queryset = Farm.objects.all()
Now you can do partial updates!

Add request/context to django in writable serializer

I want to add the request context to my serializer in the Django REST framework. In particular to a nested serializer, i (successfully) tried to do that with a SerializerMethodField ( as my solution per: context in nested serializers django rest framework ). This is the setup i use:
class VehicleTypeSerializer(RsModelSerializer):
class Meta:
model = VehicleType
class VehicleSerializer(RsModelSerializer):
vehicletype = SerializerMethodField()
class Meta:
model = Vehicle
fields = ('vehiclename', 'vehicledescription', 'vehicletype')
def get_vehicletype(self, obj):
return self.get_serializermethodfield_data(obj, VehicleType, VehicleTypeSerializer, 'vehicle')
def get_serializermethodfield_data(self, obj, model_class, serializer_class, filter_field):
filter = {filter_field: obj}
objs = model_class.objects.all().filter(**filter)
# We need the request-context for checking field permissions in the serializer
s = serializer_class(objs, many=True, context={'request': self.context.get('request')})
return s.data
Problem : I need a SerializerMethodField to pass the request-context to the nested-serializer (VehicleTypeSerializer)
But now i am stuck dealing with POST's since the SerializerMethodField is read-only. I can't POST an object to /api/v1/vehicle with:
{
"vehiclename": "test",
"vehicledescription": "test"
"vehicletype": "1" <---- get's ignored since SerializerMethodField is read-only
}
Question : Can someone point me in the right direction to add the request-context (especially the user information) to a nested serializer which i can write to?
I need the request context (request.user) in the VehicleSerializer as well as in the VechileTypeSerializer, because in the RsModelSerializer that i have defined, i check on a per-field-basis if the user that is doing the request has permission to read or update a field.
In the RsModelSerializer:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make sure that there is a user mapped in the context (we need a user
# for checking permissions on a field). If there is no user, we set
# the user to None.
if not self.context:
self._context = getattr(self.Meta, 'context', {})
try:
self.user = self.context['request'].user
except (KeyError, AttributeError):
print('No request')
self.user = None
def get_fields(self):
"""
Override get_fields to ensure only fields that are allowed
by model-field-permissions are returned to the serializer
:return: Dict with allowed fields
"""
ret = OrderedDict()
fields = super().get_fields()
# If no user is associated with the serializer, return no fields
if self.user == None:
return None
# A superuser bypasses the permissions-check and gets all
# available fields
if self.user.is_superuser:
print_without_test("user is superuser, bypassing permissions")
return fields
# Walk through all available fields and check if a user has permission for
# it. If he does, add them to a return-array. This way all fields that
# are not allowed to 'read' will be dropped. Note: this is only used
# for read access. Write access is handled in the views (modelviewsets).
for f in fields:
if has_permission(user=self.user, app_label=self.Meta.model._meta.app_label,
table=self.Meta.model.__name__.lower(),
field=f,
permission='read'):
ret[f] = fields[f]
return ret
Method-1: Overriding the __init__() method of parent serializer
You can add the context to nested/child serializer in the __init__() method of parent serializer.
class RsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(RsModelSerializer, self).__init__(*args, **kwargs)
request_obj = self.context.get('request') # get the request from parent serializer's context
# assign request object to nested serializer context
self.fields['nested_serializer_field'].context['request'] = request_obj
We cannot pass the context to nested serializer at the time of their __init__() because they get initialized at the time of declaration in the parent serializer.
class SomeParentSerializer(serializers.Serializer):
some_child = SomeChildSerializer() # gets initialized here
Method-2: Passing context when child serializer gets binded to its parent
Another option is to add the context when a child/nested serializer gets binded to the parent.
class SomeChildSerializer(Serializer):
def bind(self, field_name, parent):
super(SomeChildSerializer, self).bind(field_name, parent) # child gets binded to parent
request_obj = parent.context.get('request') # get the request from parent serializer context
self.context['request'] = request_obj
Quoting the DRF author's suggested option in the related ticket:
This should be considered private API, and the parent
__init__ style listed above should be preferred.
So, the better option is to override the __init__() method of ParentSerializer and pass the context to child/nested serializer.
(Source: check this related ticket on Github.)
If you need to pass a context to Serializer class. You can use Serializer's context
And you will be able to use it in a SerializerMethodField
class MySerializer(serializer.Serializer)
field = serializer.SerializerMethodField()
def get_field(self, obj):
return self.context.get('my_key')
You call it from view:
...
s = MySerializer(data=data, context={'my_key': 'my_value'})
...
EDIT:
If you need use this context in another Serializer class, pass to the first serializer in the pass to the nexted serializer:
# views.py
...
s = MySerializer(data=data, context={'my_key': 'my_value'})
...
# serializers.py
class MySerializer(serializer.Serializer):
field = serializer.SerializerMethodField()
def get_field(self, obj):
return MySecondSerializer(..., context=self.context)

Accessing context variables in modelform

I have a modelformset factory like so:
Form:
class AwesomeModelForm(ModelForm):
class Meta:
model = AwesomeModel
fields = ('thing', 'field_2', 'field_3')
widgets = {
'thing': HiddenInput(),
}
View:
class AwesomeView(TemplateView):
formset = modelformset_factory(AwesomeModel, form=AwesomeModelForm,
can_delete=True)
def get_context_data(self, **kwargs):
self.id = self.kwargs['id']
self.thing = Thing.objects.get(id=self.id)
formset = self.allocate_formset(queryset=AwesomeModel.objects\
.filter(thing=self.thing))
context = super(AllocateLeave, self).get_context_data(**kwargs)
context['formset'] = formset
return context
I'd like to prepopulate the modelformset_factory using initial e.g.
formset = self.allocate_formset(queryset=AwesomeModel.objects.filter(thing=self.thing), initial=[{'thing': self.thing,}])
Django seems to think that the application of "initial" means the form has changed, so if I want to change a form value but not the 'extra' form, the validation fails because the 'extra' form is incomplete.
e.g.
for form in new_formset.forms:
print form.has_changed()
returns True for the 'extra' field with the inital set and False if initial is not set in the formset declaration.
While iterating through the forms in the formset works to save each existing form, if I want to delete a form, or apply any formset wide validation, the form fails validation.
I think the solution might be to use a clean method for the 'thing' field, but I can't see how to access the context variables of the view in the modelform class. Is this the right approach or should I be pursuing another option?
Thanks

django forms: default values for bound forms

With this form:
class Form(forms.Form):
name = forms.CharField(required=False, initial='Hello world')
If I do something like this in the view:
form = Form(request.GET)
if form.is_valid():
name = form.cleaned_data['name']
Then initial value of name is lost even if request.GET does not contain name as key. Is there any workaround? I would like initial values work to bound forms as "default values".
By slightly modifying Gonzalo's solution, this is the right way:
class Form(forms.Form):
name = forms.CharField(required=False, initial='Hello world')
def clean_name(self):
if not self['name'].html_name in self.data:
return self.fields['name'].initial
return self.cleaned_data['name']
If you need this, you may have a look at django-filter app. I have discovered it quite recently.
initial isn't really meant to be used to set default values for form fields.
Instead, it's really more a placeholder utility when displaying forms to the user, and won't work well if the field isn't required (like in your example).
What you can do is define a clean_<fieldname> method that checks if there's an empty value for that field and return the default:
class Form(forms.Form):
name = forms.CharField(required=False, initial='Hello world')
def clean_name(self):
name = self.cleaned_data['name']
if name is None:
return self.fields['name'].initial
return name
I use the following pattern for setting default values as initial values given for the form-
class InitialDefaultForm(forms.Form):
def clean(self):
cleaned_data = super(InitialDefaultForm, self).clean()
# if data is not provided for some fields and those fields have an
# initial value, then set the values to initial value
for name in self.fields:
if not self[name].html_name in self.data and self.fields[name].initial is not None:
cleaned_data[name] = self.fields[name].initial
return cleaned_data
This ensures that all fields which have an initial value and do not get values from user get populated by their initial value.
request.GET is a dictionary like object.
initial only works in case of unbound form.
Forms have an attribute named data. This attribute is provided as first positional argument or as a data keyword argument during form initialization.
Bound forms are those in which you provide some data as first argument to the form and unbound form has data attribute set as None.
Here in your initialization of form form=Form(request.GET), you are providing the first positional argument, so data attribute is being set on the form and it becomes a bound form. This happens even if request.GET is an empty dictionary. And since your form becomes a bound form so initial of name field has no effect on it.
So, In you GET request you should either do:
form = Form()
and your initial of name field would be honoured.
Or, if you want to read name from request.GET and if its there then want to use it instead of field's initial then have following in your view.
name = request.GET.get(name)
form_level_initial = {}
if name:
form_level_initial['name'] = name
form = Form(initial=form_level_initial)
Will this work:
initial_form_data = {'name': 'Hello World'} #put all the initial for fields in this dict
initial_form_data.update(request.GET) #any field available in request.GET will override that field in initial_form_data
form = Form(initial_form_data)
if form.is_valid():
name = form.cleaned_data['name']
The proposed solutions either didn't work for me or just seemed not very elegant. The documentation specifies that initial does not work for a bound form, which seems to be the original questioners (and my) use case:
This is why initial values are only displayed for unbound forms. For bound forms, the HTML output will use the bound data.
https://docs.djangoproject.com/en/1.10/ref/forms/fields/#initial
My solution is to see if the form should be bound or not:
initial = {'status': [Listing.ACTIVE], 'min_price': 123} # Create default options
if request.method == 'GET':
# create a form instance and populate it with data from the request:
if len(request.GET):
form = ListingSearchForm(request.GET) # bind the form
else:
form = ListingSearchForm(initial=initial) # if GET is empty, use default form
You could also use the other ways of initializing the form (mentioned above).
None of the answers actually does exactly what clime asked for. So here is my solution for the same problem:
class LeadsFiltersForm(forms.Form):
TYPE_CHOICES = Lead.TYPES
SITE_CHOICES = [(site.id, site.name) for site in Site.objects.all()]
type = forms.MultipleChoiceField(
choices=TYPE_CHOICES, widget=forms.CheckboxSelectMultiple(),
required=False
)
site = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(), required=False,
choices=SITE_CHOICES
)
date_from = forms.DateField(input_formats=['%m-%d-%Y',], required=False,
widget=forms.TextInput(attrs={'placeholder': 'Date From'}),
initial=timezone.now() - datetime.timedelta(days=30))
date_to = forms.DateField(input_formats=['%m-%d-%Y',], required=False,
widget=forms.TextInput(attrs={'placeholder': 'Date To'}))
defaults = {
'type': [val[0] for val in TYPE_CHOICES],
'site': [val[0] for val in SITE_CHOICES],
'date_from': (timezone.now() - datetime.timedelta(days=30)).strftime('%m-%d-%Y'),
'date_to': timezone.now().strftime('%m-%d-%Y')
}
def __init__(self, data, *args, **kwargs):
super(LeadsFiltersForm, self).__init__(data, *args, **kwargs)
self.data = self.defaults.copy()
for key, val in data.iteritems():
if not data.get(key):
continue
field = self.fields.get(key)
if field and getattr(field.widget, 'allow_multiple_selected', False):
self.data[key] = data.getlist(key)
else:
self.data[key] = data.get(key)

Newbie Django Question : Can't find data I thought I preset in a Form

I'm still getting to grips with Django and, in particular, Forms.
I created MyForm which subclasses forms.Form in which I define a field like this :
owner = forms.CharField(widget=forms.HiddenInput)
When I create a new, blank instance of the Form I want to prefill this with the creator's profile, which I'm doing like this :
form = MyForm( {'owner' : request.user.get_profile()} )
Which I imagine sets the owner field of the form to the request.user's id. (The type of the corresponding "owner" field in the models.Model class is ForeignKey of Profile.)
Before rendering the form, I need to check one piece of information about the owner. So I try to access form.owner, but there seems to be no "owner" attribute of the form object. I also tried form.instance.owner, but similarly, no luck.
What am I doing wrong? What have I misunderstood?
You can access this value via the form's data dictionary:
form.data.get('owner')
Initial data in a form should be passed in with the initial kwarg.
Once you've turned the form into a bound form (usually by passing request.POST in as the first argument to instantiate the form, the place you are currently incorrectly providing the initial dictionary), and performed validation with form.is_valid(), the data the user submitted will be in form.cleaned_data, a dictionary. If they changed the initial value, their changed value will be in that dictionary. If not, your initial value will be.
If you don't want to let the user modify the value, then don't have it be a field, instead pass it in as a kwarg, and store it as an instance attribute in form.__init__():
class MyForm(Form):
def __init__(self, *args, profile, **kwargs):
super().__init__(*args, **kwargs)
self.profile = profile
...
form = MyForm(
request.POST if request.POST else None,
profile=request.user.get_profile(),
)
if request.method == "POST" and form.is_valid():
do_stuff_with(form.profile)
Also as with most things, this all gets easier if you drink the Django kool-aid and use generic views.
class MyView(FormView):
form_class = MyForm
...
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"profile": self.request.user.get_profile()
}
def form_valid(self, form):
do_stuff_with(form.profile)
return super().form_valid(form)
Or for the initial case whereby you want it to be editable:
class MyView(FormView):
form_class = MyForm
...
def get_initial(self):
return {
**super().get_initial(),
"profile": self.request.user.get_profile()
}
def form_valid(self, form):
do_stuff_with(form.cleaned_data.get("profile"))
return super().form_valid(form)
If MyForm happens to be a form about one single instance of a specific model then this gets even easier with UpdateView instead of FormView. The more you buy into the way Django wants to do things, the less you have to work.