Add form fields to django form dynamically - python

Due to a BD design where depending on a value, the data is stored in different cells, I have to add form fields dynamically. I was thinking on this:
class EditFlatForm(BaseModelForm):
on_sale = forms.BooleanField(required=False)
on_rent = forms.BooleanField(required=False)
class Meta:
model = Flat
fields = ('title', 'flat_category', 'description')
...
def __init__(self, *args, **kwargs):
super(EditFlatForm, self).__init__(*args,**kwargs)
flat_properties = FlatProperty.objects.all()
for p in flat_properties:
if p.type_value == 1:
# Text
setattr(self, p.title, forms.CharField(label=p.human_title, required=False))
elif p.type_value == 2:
# Number
setattr(self, p.title, forms.IntegerField(label=p.human_title, required=False))
else:
# Boolean
setattr(self, p.title, forms.BooleanField(label=p.human_title, required=False))
But the fields don't get added, what am I missing?

I recommend creating the form on the fly using type. So, you'll need to create a function that will generate a list of all the fields you want to have in your form and then use them to generate the form, something like this :
def get_form_class():
flat_properties = FlatProperty.objects.all()
form_fields = {}
for p in flat_properties:
if p.type_value == 1:
form_fields['field_{0}'.format(p.id)] = django.forms.CharField(...)
elif p.type_value == 2:
form_fields['field_{0}'.format(p.id)] = django.forms.IntegerField(...)
else:
form_fields['field_{0}'.format(p.id)] = django.forms.BooleanField(...)
# ok now form_fields has all the fields for our form
return type('DynamicForm', (django.forms.Form,), form_fields )
Now you can use get_form_class wherever you want to use your form, for instance
form_class = get_form_class()
form = form_class(request.GET)
if form.is_valid() # etc
For more info, you can check my post on creating dynamic forms with django:
http://spapas.github.io/2013/12/24/django-dynamic-forms/
Update to address OP's comment (But then how to gain advantage of all the things ModelForm provides?): You can inherit your dynamic form from ModelForm. Or, even better, you can create a class that is descendant of ModelForm and defines all the required methods and attributes (like clean, __init__, Meta etc). Then just inherit from that class by changing the type call to type('DynamicForm', (CustomForm,), form_fields ) !

Assuming that p.title is a string variable, then this should work:
if p.type_value == 1:
# Text
self.fields[p.title] = forms.CharField(label=p.human_title, required=False))

Related

Correct way to add dynamic form fields to WagtailModelAdminForm

I have a use case where I need to add dynamic form fields to a WagtailModelAdminForm. With standard django I would normally just create a custom subclass and add the fields in the __init__ method of the form. In Wagtail, because the forms are built up with the edit_handlers, this becomes a nightmare to deal with.
I have the following dynamic form:
class ProductForm(WagtailAdminModelForm):
class Meta:
model = get_product_model()
exclude = ['attributes', 'state', 'variant_of']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance:
self.inject_attribute_fields()
def inject_attribute_fields(self):
for k, attr in self.instance.attributes.items():
field_klass = None
field_data = attr.get("input")
field_args = {
'label': field_data['name'],
'help_text': field_data['help_text'],
'required': field_data['is_required'],
'initial': attr['value'],
}
if 'choices' in field_data:
field_args['choices'] = (
(choice["id"], choice["value"])
for choice in field_data['choices']
)
if field_data['is_multi_choice']:
field_klass = forms.MultipleChoiceField
else:
field_klass = forms.ChoiceField
else:
typ = field_data['attr_type']
if typ == 'text':
field_klass = forms.CharField
elif typ == 'textarea':
field_klass = forms.CharField
field_args['widget'] = forms.Textarea
elif typ == 'bool':
field_klass = forms.BooleanField
elif typ == 'int':
field_klass = forms.IntegerField
elif typ == 'decimal':
field_klass = forms.DecimalField
elif typ == 'date':
field_klass = forms.DateField
field_args['widget'] = AdminDateInput
elif typ == 'time':
field_klass = forms.TimeField
field_args['widget'] = AdminTimeInput
elif typ == 'datetime':
field_klass = forms.DateTimeField
field_args['widget'] = AdminDateTimeInput
if field_klass is None:
raise AttributeError('Cannot create widgets for invalid field types.')
# Create the custom key
self.fields[f"attributes__{k}"] = field_klass(**field_args)
Next I customized the ModelAdmin EditView (attributes are not present in the create view):
class EditProductView(EditView):
def get_edit_handler(self):
summary_panels = [
FieldPanel('title'),
FieldPanel('description'),
FieldPanel('body'),
]
# NOTE: Product attributes are dynamic, so we generate them
attributes_panel = get_product_attributes_panel(self.instance)
variants_panel = []
if self.instance.is_variant:
variants_panel.append(
InlinePanel(
'stockrecords',
classname="collapsed",
heading="Variants & Prices"
)
)
else:
variants_panel.append(ProductVariantsPanel())
return TabbedInterface([
ObjectList(summary_panels, heading='Summary'),
# This panel creates dynamic panels related to the dynamic form fields,
# but raises an error saying that the "fields are missing".
# Understandable because it's not present on the original model
# ObjectList(attributes_panel, heading='Attributes'),
ObjectList(variants_panel, heading='Variants'),
ObjectList(promote_panels, heading='Promote'),
ObjectList(settings_panels, heading='Settings'),
], base_form_class=ProductForm).bind_to_model(self.model_admin.model)
Here is the get_product_attributes_panel() function for reference:
def get_product_attributes_panel(product) -> list:
panels = []
for key, attr in product.attributes.items():
widget = None
field_name = "attributes__" + key
attr_type = attr['input'].get('attr_type')
if attr_type == 'date':
widget = AdminDateInput()
elif attr_type == 'datetime':
widget = AdminDateTimeInput()
else:
if attr_type is None and 'choices' in attr['input']:
if attr['input']['is_multi_choice']:
widget = forms.SelectMultiple
else:
widget = forms.Select
else:
widget = forms.TextInput()
if widget:
panels.append(FieldPanel(field_name, widget=widget))
else:
panels.append(FieldPanel(field_name))
return panels
So the problem is...
A) Adding the ProductForm in the way I did above (by using it as the base_form_class in TabbedInterface) almost works; It adds the fields to the form; BUT I have no control over the rendering.
B) If I uncomment the line ObjectList(attributes_panel, heading='Attributes'), (to get nice rendering of the fields), then I get an error for my dynamic fields, saying that they are missing.
This is a very important requirement in the project I'm working on.
A temporary workaround is to create a custom panel to render the dynamic fields directly in the html template; But then I lose the Django Form validation, which is also an important requirement for this.
Is there any way to add dynamic fields the the WagtailModelAdminForm, that preserves the modeladmin features such as formsets, permissions etc.
I ended up creating a separate AttributeForm for the attributes.
A custom Panel then looks for this new form instance as an attribute of the primary form. As an instance on the primary form, I can "clean" this internal form when the primary form clean() is called and, raise any errors that I need to on both forms.
I then customized the EditView.post() method to make sure that I add the instance of AttributesForm to our primary model form.
It's a bit of a workaround, but works well enough for now. I wish there was an easier way, but it doesn't look like it right now.

Django: Pass a variable/parameter to form from view? [duplicate]

I have a Model as follows:
class TankJournal(models.Model):
user = models.ForeignKey(User)
tank = models.ForeignKey(TankProfile)
ts = models.IntegerField(max_length=15)
title = models.CharField(max_length=50)
body = models.TextField()
I also have a model form for the above model as follows:
class JournalForm(ModelForm):
tank = forms.IntegerField(widget=forms.HiddenInput())
class Meta:
model = TankJournal
exclude = ('user','ts')
I want to know how to set the default value for that tank hidden field. Here is my function to show/save the form so far:
def addJournal(request, id=0):
if not request.user.is_authenticated():
return HttpResponseRedirect('/')
# checking if they own the tank
from django.contrib.auth.models import User
user = User.objects.get(pk=request.session['id'])
if request.method == 'POST':
form = JournalForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
# setting the user and ts
from time import time
obj.ts = int(time())
obj.user = user
obj.tank = TankProfile.objects.get(pk=form.cleaned_data['tank_id'])
# saving the test
obj.save()
else:
form = JournalForm()
try:
tank = TankProfile.objects.get(user=user, id=id)
except TankProfile.DoesNotExist:
return HttpResponseRedirect('/error/')
You can use Form.initial, which is explained here.
You have two options either populate the value when calling form constructor:
form = JournalForm(initial={'tank': 123})
or set the value in the form definition:
tank = forms.IntegerField(widget=forms.HiddenInput(), initial=123)
Other solution: Set initial after creating the form:
form.fields['tank'].initial = 123
If you are creating modelform from POST values initial can be assigned this way:
form = SomeModelForm(request.POST, initial={"option": "10"})
https://docs.djangoproject.com/en/1.10/topics/forms/modelforms/#providing-initial-values
I had this other solution (I'm posting it in case someone else as me is using the following method from the model):
class onlyUserIsActiveField(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(onlyUserIsActiveField, self).__init__(*args, **kwargs)
self.fields['is_active'].initial = False
class Meta:
model = User
fields = ['is_active']
labels = {'is_active': 'Is Active'}
widgets = {
'is_active': forms.CheckboxInput( attrs={
'class': 'form-control bootstrap-switch',
'data-size': 'mini',
'data-on-color': 'success',
'data-on-text': 'Active',
'data-off-color': 'danger',
'data-off-text': 'Inactive',
'name': 'is_active',
})
}
The initial is definded on the __init__ function as self.fields['is_active'].initial = False
As explained in Django docs, initial is not default.
The initial value of a field is intended to be displayed in an HTML . But if the user delete this value, and finally send back a blank value for this field, the initial value is lost. So you do not obtain what is expected by a default behaviour.
The default behaviour is : the value that validation process will take if data argument do not contain any value for the field.
To implement that, a straightforward way is to combine initial and clean_<field>():
class JournalForm(ModelForm):
tank = forms.IntegerField(widget=forms.HiddenInput(), initial=123)
(...)
def clean_tank(self):
if not self['tank'].html_name in self.data:
return self.fields['tank'].initial
return self.cleaned_data['tank']
If you want to add initial value and post other value you have to add the following :
or None after request.POST
form = JournalForm(request.POST or None,initial={'tank': 123})
If you want to add files or images also
form = JournalForm(request.POST or None,request.FILES or None,initial={'tank': 123})
I hope this can help you:
form.instance.updatedby = form.cleaned_data['updatedby'] = request.user.id
I also encountered the need to set default values in the form during development. My solution is
initial={"":""}
form=ArticleModel(request.POST)
if form.has_changed():
data = {i: form.cleaned_data[i] for i in form.changed_data}
data.update({key: val for key, val in init_praram.items() if key not in form.changed_data})
use form.has_changed ,if form.fields is required you can use this method
How I added the initial to the form:
I read #Sergey Golovchenko answer.
So I just added it to the form in if request.method == 'POST':.
But that's not where you place it, if you want to see what value it got before posting the form.
You need to put it in the form where the else is.
Example here from views.py
def myForm(request):
kontext = {}
if request.method == 'POST':
# You might want to use clean_data instead of initial here. I found something on a stack overflow question, and you add clean data to the Forms.py, if you want to change the post data. https://stackoverflow.com/questions/36711229/django-forms-clean-data
form = myModelForm(request.POST, initial={'user': request.user})
if form.is_valid():
form.save()
return redirect('/')
else:
# you need to put initial here, if you want to see the value before you post it
form = myModelForm(initial={'user': request.user})
kontext['form'] = form
return render(request, 'app1/my_form.html', kontext)

ModelForm saving over model data with empty fields

I'm building an Edit form for a model in my database using a ModelForm in Django. Each field in the form is optional as the user may want to only edit one field.
The problem I am having is that when I call save() in the view, any empty fields are being saved over the instance's original values (e.g. if I only enter a new first_name, the last_name and ecf_code fields will save an empty string in the corresponding instance.)
The form:
class EditPlayerForm(forms.ModelForm):
class Meta:
model = Player
fields = ['first_name', 'last_name', 'ecf_code']
def __init__(self, *args, **kwargs):
super(EditPlayerForm, self).__init__(*args, **kwargs)
self.fields['first_name'].required = False
self.fields['last_name'].required = False
self.fields['ecf_code'].required = False
The view:
def view(request, player_pk = ''):
edit_player_form = forms.EditPlayerForm(auto_id="edit_%s")
if "edit_player_form" in request.POST:
if not player_pk:
messages.error(request, "No player pk given.")
else:
try:
selected_player = Player.objects.get(pk = player_pk)
except Player.DoesNotExist:
messages.error(request, "The selected player could not be found in the database.")
return redirect("players:management")
else:
edit_player_form = forms.EditPlayerForm(
request.POST,
instance = selected_player
)
if edit_player_form.is_valid():
player = edit_player_form.save()
messages.success(request, "The changes were made successfully.")
return redirect("players:management")
else:
form_errors.convert_form_errors_to_messages(edit_player_form, request)
return render(
request,
"players/playerManagement.html",
{
"edit_player_form": edit_player_form,
"players": Player.objects.all(),
}
)
I've tried overriding the save() method of the form to explicitly check which fields have values in the POST request but that didn't seem to make any difference either.
Attempt at overriding the save method:
def save(self, commit = True):
# Tried this way to get instance as well
# instance = super(EditPlayerForm, self).save(commit = False)
self.cleaned_data = dict([ (k,v) for k,v in self.cleaned_data.items() if v != "" ])
try:
self.instance.first_name = self.cleaned_data["first_name"]
except KeyError:
pass
try:
self.instance.last_name = self.cleaned_data["last_name"]
except KeyError:
pass
try:
self.instance.ecf_code = self.cleaned_data["ecf_code"]
except KeyError:
pass
if commit:
self.instance.save()
return self.instance
I also do not have any default values for the Player model as the docs say the ModeForm will use these for values absent in the form submission.
EDIT:
Here is the whole EditPlayerForm:
class EditPlayerForm(forms.ModelForm):
class Meta:
model = Player
fields = ['first_name', 'last_name', 'ecf_code']
def __init__(self, *args, **kwargs):
super(EditPlayerForm, self).__init__(*args, **kwargs)
self.fields['first_name'].required = False
self.fields['last_name'].required = False
self.fields['ecf_code'].required = False
def save(self, commit = True):
# If I print instance variables here they've already
# been updated with the form values
self.cleaned_data = [ k for k,v in self.cleaned_data.items() if v ]
self.instance.save(update_fields = self.cleaned_data)
if commit:
self.instance.save()
return self.instance
EDIT:
Ok so here is the solution, I figured I'd put it here as it might be useful to other people (I've certainly learned a bit from this).
So it turns out that the is_valid() method of the model form actually makes the changes to the instance you pass into the form, ready for the save() method to save them. So in order to fix this problem, I extended the clean() method of the form:
def clean(self):
if not self.cleaned_data.get("first_name"):
self.cleaned_data["first_name"] = self.instance.first_name
if not self.cleaned_data.get("last_name"):
self.cleaned_data["last_name"] = self.instance.last_name
if not self.cleaned_data.get("ecf_code"):
self.cleaned_data["ecf_code"] = self.instance.ecf_code
This basically just checks to see if the fields are empty and if a field is empty, fill it with the existing value from the given instance. clean() gets called before the instance variables are set with the new form values, so this way, any empty fields were actually filled with the corresponding existing instance data.
You could maybe use the update() method instead of save()
or the argument update_field
self.instance.save(update_fields=['fields_to_update'])
by building the list ['fields_to_update'] only with the not empty values.
It should even work with the comprehension you've tried :
self.cleaned_data = [ k for k,v in self.cleaned_data.items() if v ]
self.instance.save(update_fields=self.cleaned_data)
EDIT :
Without overriding the save method (and commenting out this attempt in the form):
not_empty_data = [ k for k,v in edit_player_form.cleaned_data.items() if v ]
print(not_empty_data)
player = edit_player_form.save(update_fields=not_empty_data)
You could check the values if it's not empty in your view without overriding save()
if edit_player_form.is_valid():
if edit_player_form.cleaned_data["first_name"]:
selected_player.first_name = edit_player_form.cleaned_data["first_name"]
if edit_player_form.cleaned_data["last_name"]:
selected_player.last_name= edit_player_form.cleaned_data["last_name"]
if edit_player_form.cleaned_data["ecf_code"]:
selected_player.ecf_code= edit_player_form.cleaned_data["ecf_code"]
selected_player.save()
This should work fine with what you want. I'm not sure if it's the best way to do it but it should work fine.

Django Form Wizard For Dynamic Sized Forms

So I have a dynamic form which can have an arbitrary size from one field to 100 or more and I was wondering how it would be possible to use Django's form wizard for that dynamic form. The fields are generated in the __init__ of the form by adding them to the fields dictionary.
class MyDynamicForm(forms.Form):
def __init__(self, *args, **kwargs):
parent = kwargs.pop('parent')
super(MyDynamicForm, self).__init__(*args, *kwargs)
# Add each element into self.fields
for element in parent.elements:
if element.type == TEXT_FIELD:
self.fields[element.name] = forms.CharField(widget=forms.Textarea)
elif element.type == CHECKBOX_FIELD:
self.fields[element.name] = forms.BooleanField()
elif element.type == SINGLE_CHOICE_FIELD:
self.fields[element.name] = forms.ChoiceField(choices=element.choices.split(','))
elif element.type = MULTIPLE_CHOICE_FIELD:
self.fields[element.name] = forms.MultipleChoiceField(choices=element.choices.split(','))
I assume that I could wrap this form class in a function and only have it return the form class which only creates a portion of the fields by doing for element in parent.elements[start:end] rather than what I'm doing to create each wizard class but I feel like this is not the correct approach. How should I go about this, is there a correct way? Or is it even possible? Thanks!

How to initialize django-form data type field from a given choices?

First of all: I am not able to find out the proper Title of this question.
Anyhow the question is:
I have to fill a form at template and the fields of this form are user dependent. For example you passes integer (integer is not a datatype) as a parameter to the method and it should returns like this:
fileds = forms.IntegerField()
If you pass bool then it should like this:
fields = forms.BooleanField()
So that i can use them to create my form. I tried with this code but it returns into the form of string.
Some.py file:
choices = (('bool','BooleanField()'),
('integer','IntegerField()'))
def choose_field():
option = 'bool' # Here it is hardcoded but in my app it comes from database.
for x in choices:
if x[0]==option:
type = x[1]
a = 'forms'
field = [a,type]
field = ".".join(field)
return field
When i print the field it prints 'forms.BooleanField()'. I also use this return value but it didn't work. Amy solution to this problem?
The simpliest way is to create your form class and include fields for all possible choices to it. Then write a constructor in this class and hide the fields you don't want to appear. The constructor must take a parameter indicating which fields do we need. It can be useful to store this parameter in the form and use it in clean method to correct collected data accordingly to this parameter.
class Your_form(forms.ModelForm):
field_integer = forms.IntegerField()
field_boolean = forms.BooleanField()
def __init__(self, *args, **kwargs):
option = kwargs["option"]
if option == "integer":
field_boolean.widget = field_boolean.hidden_widget()
else:
field_integer.widget = field_integer.hidden_widget()
super(Your_form, self).__init__(*args, **kwargs)
In your controller:
option = 'bool'
form = Your_form(option=option)

Categories

Resources