Usually, in a wizard, we declare forms or formsets in static way, for example with something like:
form_class=formset_factory(MyForm, min_num=1, extra=5) # let's say this is step '4'
But now, what if I need data from step 3, to know how to define the min_num or extra value for the formset of step 4?
I was thinking of doing such thing in the get_form() method:
def get_form(self, step=None, data=None, files=None):
form = super().get_form(step, data, files)
# ....
elif step == '4':
step3_data = self.storage.get_step_data('3')
# ... here I would parse step 3 data, to be able to define:
computed_step_4_min_num = 5
computed_step_4_extra = 10
# And here I would need to call formset_factory(min_num=computed_step_4_min_num,
extra=computed_step_4_extra),
# but how? This is obviously not the right place for this call.
While it's easy to edit form fields attributes in the get_form() method, I did not find a way to define the right number of forms of a formset, in a dynamic way.
I read documentation but I could have missed it. Thanks for your help.
By reading the documentation and checking the source code, I think this is the optimal solution to use:
def get_form(self, step=None, data=None, files=None):
if step == '4':
step3_data = self.storage.get_step_data('3')
# do calculations
computed_step_4_min_num = 5
computed_step_4_extra = 10
form = formset_factory(MyForm, min_num=computed_step_4_min_num, extra=computed_step_4_extra)
self.form_list[step] = form
return super().get_form(step, data, files)
I am overriding the self.form_list to add the formset factory. But you should add a formset in the view when initiating the Wizard instance:
>> formset = formset_factory(MyForm, min_num=1, extra=1)
>> MyWizardForm.as_view([Form1, Form2, Form3, formset], initial_dict=initial)
Related
When working with Django model forms, I often do something like this:
def my_view(request):
new_entry = MyModel(name='a')
form = MyModelForm(instance=new_entry)
...
I want to do something similar with a modelformset. Something like this would be ideal:
def my_view(request):
MyFormSet = modelformset_factory(MyModel, form=MyModelForm)
new_entries = [MyModel(name='a'), MyModel(name='b')]
formset = MyFormSet(instances=new_entries) # off course this does not work
...
Since the items are not stored in the database yet, I can't set the instances using a queryset. If I want to use initial I have to declare the fields in the form, which is not ideal and seems a bit hacky.
Any suggestions how I can set the instances of each modelform in a modelformset?
Ok, I think I've found a solution.
class FormSetWithInstances(BaseFormSet):
def __init__(self, *args, **kwargs):
self.instances = kwargs.pop('instances')
super(FormSetWithInstances, self).__init__(*args, **kwargs)
def get_form_kwargs(self, index):
form_kwargs = super(FormSetWithInstances, self).get_form_kwargs(index)
if index < len(self.instances):
form_kwargs['instance'] = self.instances[index]
return form_kwargs
Be careful when using this modelformsets or inlinemodelformsets, as the queryset will override the instance you set.
An alternative approach:
class FormSetWithInstances(BaseFormSet):
def get_form_kwargs(self, index):
kwargs = super(FormSetWithInstances, self).get_form_kwargs(index)
instances = kwargs.pop('instances')
try:
kwargs.update({'instance': instances[index]})
except IndexError:
pass
return kwargs
Then, when creating instances of FormSetWithInstances, you pass in a list of instances as a form kwarg:
form_set = FormSetWithInstances(form_kwargs={'instances': [...]})
I personally prefer this method because it takes advantage of existing class infrastructure instead of defining custom class members in an overridden __init__(). Also, it's in the docs.
I'm not aware of an easy way to pass a list of instances as you are trying to do. Here's a couple of options that might work depending on your use case.
You can provide initial data for the model formset. This should be a list of dictionaries, not model instances:
initial = [{'name': 'a'}, {'name': 'b'}]
formset = MyFormSet(
queryset=MyModel.objects.none(),
initial=initial,
)
Note that I have set the queryset to an empty queryset. If you didn't do this, then the formset would display existing instances, and the initial data would be used for new instances.
If you have initial values for fields that you do not wish to include in the form, then you could be able to set those values when you [save the formset].
instances = formset.save(commit=False)
names = ['a', 'b']
for instance, name in zip(instances, names):
instance.name = name
instance.save()
I am trying to create a SessionWizardView for a trip creation process. The trip might have one leg (one way) or two legs (round trip). Each leg has similar schema so I would like to use the same Form for both step 0 and step1, with a condition saying only to use step1 when the flight is round trip.
The problem I am having is that my "submit" button keeps loading step 0 over and over again instead of moving on to step 1 as it should for a round trip flight. (I am prepopulating each of the forms based on previously requested trip info for each leg in the get_form_initial() override). My form populates correctly for the first leg, it just populates the first leg data on every submit ad infinitum.
I could make two identical forms, but that seems like poor practice. Slightly better, I could have the Return trip form just inherit from the Outbound trip form and not make any changes to it - this is what I'll try next barring a better solution.
But, I'm really wondering if there is there a way to use the same form twice?
In my urls.py:
wizard_forms = [TripCreationForm,TripCreationForm]
urlpatterns = patterns('',
url(r'^trip/wizard/(?P<pk>\d+)$',
views.CreateTripSetView.as_view(wizard_forms,
condition_dict= {'1':show_return_trip_form}), name='admin_add_tripset')
)
in views.py:
def show_return_trip_form(wizard):
"""
Tells the CreateTripSetView wizard whether to show the return trip form
Args:
wizard:
Returns: True if this is a round trip, false if one-way
"""
cleaned_data = wizard.get_cleaned_data_for_step('0') or {}
if cleaned_data.get('total_legs') == 2:
return True
return False
class CreateTripSetView(SessionWizardView):
def get_form_initial(self, step):
"""
Populates the initial form data based on the request, route etc.
THIS IS ALWAYS FIRING FOR STEP=0 WHEN I HIT SUBMIT.
Args:
step:
Returns:
"""
initial = self.initial_dict.get(step, {})
triprequest = TripRequest.objects.filter(id=self.kwargs['pk']).first()
if triprequest is None:
return initial
initial.update({
'request_id': flight_request.id,
#other fields set on initial here
})
return initial
in forms.py:
class TripCreationForm
#field defs ex.
request_id = forms.IntegerField()
#etc.
def __init__(self, initial, *args, **kwargs):
object_data = {}
object_data['request_id'] = initial['request_id']
#etc.
super(AnywhereFlightCreationForm, self).__init__(initial=object_data, *args, **kwargs)
Edited:
So far I've been able to make this work using two subclasses of TripCreationForm but not using TripCreationForm for both.
Thanks in advance!
The wizard needs to identify them as separate steps. Maybe this would work?
wizard_forms = [
("form1", TripCreationForm),
("form2", TripCreationForm),
]
Is the any solution to get django's user_full_name as a initial value for form? My idea was to display a django's form on the end of shopping to finish a order. I want also do put into a form total value, but this is for later.
I did something like this:
user_dane = request.user.get_full_name
koszyk = request.session.get('koszyk', [])
produkty = list(Produkt.objects.filter(pk__in=koszyk))
suma_cen = Produkt.objects.filter(pk__in=koszyk).aggregate(suma=Sum('cena'))
suma_wszystkich_cen = suma_cen['suma']
form=ZamowienieForm(initial={'imie_nazwisko':user_dane, 'kwota_do_zaplaty':suma_wszystkich_cen})
but this is working only when request.method is POST.
if request.method =='POST':
form = ZamowienieForm()
According to documentation I shouldn't initial a empty form with POST... Is there any chance to have a user full name into a form?
Here is the form class:
class ZamowienieForm(forms.ModelForm):
class Meta:
model = Zamowienie
fields = ('imie_nazwisko', 'kwota_do_zaplaty', 'miejscowosc',
'ulica','numer_domu', 'numer_mieszkania', 'kod_pocztowy',)
class NewMeta:
readonly = ('imie_nazwisko','kwota_do_zaplaty',)
Maybe try something like this inside ZamowienieForm class
def __init__(self, *args, **kwargs):
super(ZamowienieForm, self).__init__(*args, **kwargs)
self.fields['imie_nazwisko'] = self.initial.get('imie_nazwisko')
self.fields['kwota_do_zaplaty'] = self.initial.get('kwota_do_zaplaty')
Although I don't understand why "initial" is not working out of the box
In this case, you only need to initialize your form once, and not inside a conditional check if the request is a GET or POST:
def your_view(request):
form = ZamowienieForm(
request.POST or None,
initial={'imie_nazwisko': request.user.get_full_name(),
'kwota_do_zaplaty': suma_wszystkich_cen}
)
if request.method == 'POST' and form.is_valid():
# do whatever
This way you are always passing in the initial value, and if request.method == 'GET', then None is passed as the first positional argument to the form.
Also, user.get_full_name is an instance method, not a property, so using request.user.get_full_name only returns the bound method, not the actual value. You have have to call the function using ()
Finally, this will only work for users that are authenticated. The anonymous user object in Django won't return any user-specific information.
I've been struggling with this problem and can't seem to find a solution...
I have a FormWizard which contains two steps: a Form (step0), then a FormSet (step1):
forms.py
# Step 0
class StrategyForm(forms.ModelForm):
class Meta:
model = Strategy
# Step 1
class LegForm(forms.ModelForm):
class Meta:
model = Leg
LegFormSet = formset_factory(LegForm, max_num=10)
In step0, the user enters a number, which I would like to use in the FormSet as the "extra", i.e. if the user inputs 10 in step0, I would like the FormSet to appear with 10 Forms in the next step.
I have tried the following based on other questions, but can't seem to find a solution:
views.py
def get_form(self, step=None, data=None, files=None):
if step is None:
step = self.steps.current
form = super(DealWizard, self).get_form(step, data, files)
if step == 'leg':
form.extra = 10
return form
and
def get_form(self, step=None, data=None, files=None):
if step is None:
step = self.steps.current
form = super(DealWizard, self).get_form(step, data, files)
if step == 'leg':
mgmt_form = form.management_form
mgmt_form.initial[INITIAL_FORM_COUNT] = 10
mgmt_form.initial[TOTAL_FORM_COUNT] = 10
return form
I'm (very) new to django, so any advice will help :)
Try this:
def get_form(self, step=None, data=None, files=None):
if step is None:
step = self.steps.current
if step == "1":
prev_data = self.get_cleaned_data_for_step(self.get_prev_step(
self.steps.current))
cnt = prev_data['extra_from_step0'] #use appropriate key as per your form
return formset_factory(LegForm, extra=cnt, max_num=cnt)
return super(YourFormWizard, self).get_form(step, data, files)
It overrides get_form() method to get count from previous step and return formset_factory() with those many forms using max_num.
As side node, you may want to use modelformset_factory().
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!