Django Form Wizard persistent initial data between steps - python

I'm using Django Form Wizard in project. I want to allow users to provide initial data for specific field and I also want to make field with initial data provided disabled.
Problem is that when user clicks back button the initial data is erased.
For example this is my form(initial data is set as second step)
As you can see everything is fine here, the form fields are disabled and the value is selected. Now we click next.
We click next and we get to the second step without problems but now if I click back..
Form fields are still disabled but the value is gone!
The code I'm using goes like this (relevant part):
Form
CHOICES=[('ONE','Choice 1'),
('TWO','Choice 2'),
('THREE','Choice 3'),
('FOUR','Choice 4'),
]
class BookingForm1(forms.Form):
""" Select reservation type"""
sko = forms.ChoiceField(choices=CHOICES, widget=forms.RadioSelect())
Wizard
def get_form(self, step=None, data=None, files=None):
form = super(PresentView, self).get_form(step, data, files)
step = step or self.steps.current
initial = {
'sko': {
'initial':'TWO'
}
}
for field in initial:
try:
form.fields[field].widget.attrs['readonly'] = True
form.fields[field].widget.attrs['disabled'] = True
form.fields[field].required = False
form.fields[field].initial = initial[field]['initial']
except:
pass
Any ideas would be greatly appreciated!

The way I was able to solve it was by removing extra fields for ChoiceField type objects.
for field in initial:
try:
field_o = form.fields[field]
# Remove extra choices if Choice field
if type(field_o) == forms.fields.ChoiceField:
field_o.choices = [(key, value) for key,value in field_o.choices if key == initial[field]['initial']]
else:
field_o.widget.attrs['readonly'] = True
field_o.initial = initial[field]['initial']
except:
pass

Related

Django 4 update form fields dynamically using HTMX

I developed a Django application in which i have a form with some fields. Depending on the input additional fields are displayed are hidden. Now everything worked quit fine in Django 3.2.14 since the update in Django 4.0.6 it didn't worked anymore.
I first build a form, where if a "field_rule_display" exists the field widget is set as "HiddenInput".
class AnalysisForm(forms.Form):
def __init__(self, analysis_form_template: AnalysisFormTemplate, disable_required: bool, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout()
self.helper.add_input(Submit("submit", _("Evaluate"), css_class="btn-primary btn-lg"))
analysis_field_queryset = analysis_form_template.analysis_fields
analysis_form_url = reverse("analysis_form", args=(analysis_form_template.id,))
for field in analysis_field_queryset.all():
htmx_dictionary = _htmx_dictionary(analysis_form_url, field)
self.fields[field.name_for_formula] = _get_field_by_type(
field, htmx_dictionary, analysis_form_template, self.data
)
self.fields[field.name_for_formula].empty_values = empty_values()
self.helper.layout.fields.append(
Div(Field(field.name_for_formula), css_class=AnalysisFieldKind(field.kind).name)
)
if field.field_rule_display is not None and disable_required is False:
self.fields[field.name_for_formula].widget = forms.HiddenInput()
self.fields[field.name_for_formula].widget.attrs["disabled"] = True
if disable_required:
self.fields[field.name_for_formula].required = False
After the user enters a specific input into the form, htmx will send a request and i rebuild the form with the new fields. And here the problem starts, even if i update my field in the "self.fields" Django does not render the update and my form field is still hidden.
if field.field_rule_display is not None:
evaluated_result_display = self._evaluated_formula(
field,
analysis_form_template,
field.field_rule_display,
field.field_rule_display.formula,
cleaned_data,
)
if evaluated_result_display:
field_type = _get_field_by_type(
field, htmx_dictionary, analysis_form_template, cleaned_data
)
self.fields[field.name_for_formula] = field_type
self.fields[field.name_for_formula].initial = cleaned_data[field.name_for_formula]
Here the second field should be displayed, but only my border is shown as a result of changing the crispy layout helper.
if (
field.field_rule_display is None or (field.field_rule_display is not None and evaluated_result_display)
) and field.field_rule_highlight is not None:
evaluated_result_highlight = self._evaluated_formula(
field.name_for_formula,
analysis_form_template,
field.field_rule_highlight,
field.field_rule_highlight.formula,
cleaned_data,
)
if evaluated_result_highlight:
field_layout = Div(
Field(field.name_for_formula),
css_class=f"{AnalysisFieldKind(field.kind).name} border border-primary mb-2 p-2",
)
self.fields[field.name_for_formula].empty_values = empty_values()
field_layout_list.append(field_layout)
self.helper.layout.fields = field_layout_list
I would appreciate some help, why this does not work anymore in Django 4 but in Django 3 it worked without a problem.
After some debugging i found a possible solution to my problem.
It seems that Django caches my form fields and their widgets in "_bound_fields_cache". Now there my fields widget is still set to "HiddenInput", even after updating the widget to a "TextInput".
So i tried updating the field in "_bound_fields_cache" and to my suprise it worked.
if field.field_rule_display is not None:
evaluated_result_display = self._evaluated_formula(
field,
analysis_form_template,
field.field_rule_display,
field.field_rule_display.formula,
cleaned_data,
)
if evaluated_result_display:
field_type = _get_field_by_type(
field, htmx_dictionary, analysis_form_template, cleaned_data
)
self.fields[field.name_for_formula] = field_type
self.fields[field.name_for_formula].initial = cleaned_data[field.name_for_formula]
if field.name_for_formula in self._bound_fields_cache:
self._bound_fields_cache[field.name_for_formula].field = field_type
But i am not quite satisfied with this kind of solution. I can't really point my finger why Django doesn't change the "_bound_fields_cache" after updating my form fields. Changing the "_bound_fields_cache" seems for me like an ugly hack... Is there perhaps a better solution?

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)

How can I pass an ID across function views and CBV? - Django

Sorry for such a rookie question, but I'm stuck and I'm reaching out. I've used the FormWizard to capture subscription data from users. Works great. Now I want them to be able to update their subscription using the same FormWizard.
I'm able to show their previous inputs, however, when it comes to actually knowing which record to update, that's where I'm having trouble. I am able to get the id from the URL in the view function for that path, but I'm having trouble getting the id to other views.
My code is below. I'm stuck on section 9.3. I'm not sure how to get the record id so it can update the correct record. If there is a better approach, feel free to suggest it and thanks in advance.
urls.py
path('subscription/update/<int:id>/', service_views.wizard_edit, name='wizard-edit'),
views.py
## 9.1 Displaying the data in the form
def wizard_edit(request, id):
## Collecting the data
sub = Subscribers.objects.get(id=id)
## Displaying the data in the appropriate forms
initial = {
'0': {'industry_search':sub.industry},
'1': {'city':sub.city, 'kilometers':sub.kilometers, 'street_1':sub.street_1, 'street_2':sub.street_2},
'2': {'email':sub.email}
}
wiz = ContactWizardUpdate.as_view([ContactForm1, ContactForm2, ContactForm3], initial_dict=initial)
return wiz(request)
## 9.2 FormWizard
class ContactWizardUpdate(SessionWizardView):
template_name = 'service/subscribe.html'
form_list = [ContactForm1, ContactForm2, ContactForm3]
def done(self, form_list, **kwargs):
## Function to update the DB and save data
update_the_form_data(self, form_list)
return render(self.request, 'service/done.html')
## 9.3 Updating the database with the changes
def update_the_form_data(self, form_list):
form_data = [form.cleaned_data for form in form_list]
## Get the correct record for the update
foo = get_object_or_404(Subscribers, id=[THE ID FOR THE RECORD])
## Additional code
foo.save()
I figured out a way to do it. I'll share in case this helps someone else.
Instead of having the path on the urls.py file:
path('subscription/update/<int:id>/', service_views.wizard_edit, name='wizard-edit'),
I changed it to, path('subscription/update/', service_views.wizard_edit, name='wizard-edit'), and added an edit button on the user's subscription summary page with the following path, Edit Subscription
Here are the edits to the views.py file:
## 9.1 Displaying the data in the form
def wizard_edit(request): ## NEW
id_ = request.GET.get('id') ## NEW
## Collecting the data
sub = Subscribers.objects.get(id=id_) ## NEW
## Displaying the data in the appropriate forms
initial = {
'0': {'industry_search':sub.industry},
'1': {'city':sub.city, 'kilometers':sub.kilometers, 'street_1':sub.street_1, 'street_2':sub.street_2},
'2': {'email':sub.email}
}
wiz = ContactWizardUpdate.as_view([ContactForm1, ContactForm2, ContactForm3], initial_dict=initial)
return wiz(request)
## 9.2 FormWizard
class ContactWizardUpdate(SessionWizardView):
template_name = 'service/subscribe.html'
form_list = [ContactForm1, ContactForm2, ContactForm3]
def done(self, form_list, **kwargs):
## Function to update the DB and save data
update_the_form_data(self, form_list)
return render(self.request, 'service/done.html')
## 9.3 Updating the database with the changes
def update_the_form_data(self, form_list):
form_data = [form.cleaned_data for form in form_list]
id_ = self.request.GET.get('id') ## NEW
## Get the correct record for the update
foo = get_object_or_404(Subscribers, id=id_) ## NEW
## Additional code
foo.save()

Can I use same Form twice for django SessionWizard?

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),
]

iterate over django form results (not in a template)

I am trying to iterate over form results and I can't help but think that I am re-inventing the wheel here.
filterlist = []
if request.POST:
form = FilterForm(request.POST)
if form.is_valid():
for key, value in form.cleaned_data.iteritems():
filterlist.append(key)
filterlist.append(value)
This works, but seems very awkward and creates lots of other problems. For example the values come back with u' so I have to use value.encode("utf8") but then if a value is None it throws in error. So now I have to check if it is None, if not then encode. There has to be a better way.
EDIT: What I am trying to do.
I am trying to filter what is shown on a page. The problem I am running into is that if a value is empty (the user don't fill the box because they only want to filter against one object) then I get no results. For example a user wants to search for all books by the author name "Smith" but doesn't want to search against a genre.
results = Books.objects.filter(author=author, genre=genre)
The user would get no results because this is an AND search. But, if a user put in "Smith" for the author and "mystery" for the genre then it works exactly like I want it to, only giving results where both are true.
So, I am trying to eliminate the empty stuff by iterating over the form results. Like I said I am probably re-inventing the wheel here.
In Python 3 use:
for key, value in form.cleaned_data.items():
If the field names are the same in the model and the form, try this:
filter = {}
if request.method == 'POST':
form = FilterForm(request.POST)
if form.is_valid():
for key, value in form.cleaned_data.iteritems():
if value:
filter[key] = value
results = Books.objects.filter(**filter)
Python is one of the few languages having named parameters. You can assemble a dict with the non-empty form fields and pass it to the filter method using the kwargs unpacking operator **.
For example:
kwargs = {"author": "Freud"}
results = Books.objects.filter(**kwargs)
Is the same as:
results = Books.objects.filter(author="Freud")
I think the problem is that by default the Model form is not valid if a form field does not have a value entered by the user, if you don`t require the field every time from the user you need to set the required field to false in the ModelForm class in forms.py as shown in the code below. Remember that the field is set false only in the model form not in the model itself
class myForm(forms.ModelForm):
myfield_id = forms.CharField(required=False)
myfield_foo = forms.CharField(required=False)
myfield_bar = forms.CharField(required=False)
myfield_name = forms.CharField(required=False)
class Meta:
model = myModel
exclude = ('myfield_ex','myfield_file')
fields = ['myfield_id','myfield_foo','myfield_bar','myfield_name',]
After you have the form entered by the user what you need is use the Q object which can be used to create complex queries as described in the manula page here
https://docs.djangoproject.com/en/1.7/topics/db/queries/#complex-lookups-with-q
A simple example code would look like
if form.is_valid():
qgroup = []
for key,value in form.cleaned_data.iteritems():
if value:
q_name = Q(**{"%s"%format(filterKey[key]) : value})
qgroup.append(q_name)
q = None
# can use the reduce as shown here qgroup = reduce(operator.or_, (Q(**{"{0}".format(filterKey[key]): value}) for (key,value) in form.cleaned_data.iteritems()))
for key,value in form.cleaned_data.iteritems():
if value:
q_name = Q(**{"%s"%format(filterKey[key]) : value})
qgroup.append(q_name)
for x in qgroup:
q &= x ### Or use the OR operator or
if q:
resultL = myModel.objects.filter(q).select_related()
The filterKey can look something on the lines of
filterKey = {'myfield_id' : "myfield_id",
'myfield_foo' : "myfield_foo__icontains",
'myfield_bar' : "myfield_bar__relative_field__icontains",
}

Categories

Resources