My Goal
I have a django project with a form, and I want to display a preview page before the user submits.
The problem
I can display a preview page using a Django FormPreview, but not all form data is displayed properly. Specifically, if I have a field with choices, the string values of these choices aren't displayed. I'm also having problems applying template filters to date fields. The end result is that some data on the preview page is visible but other data is blank:
However, if I display the same data for posts that have actually been submitted, then everything displays properly:
My Code
models.py:
class Game(models.Model):
# Game Choices
FOOTBALL = 0
BASKETBALL = 1
TENNIS = 2
OTHER = 3
GAME_CHOICES = (
(FOOTBALL, 'Football'),
(BASKETBALL, 'Basketball'),
(TENNIS, 'Tennis'),
(OTHER, 'Other')
)
game_id = models.AutoField(primary_key=True)
location = models.CharField(max_length=200, verbose_name="Location")
game = models.IntegerField(choices=GAME_CHOICES, default=FOOTBALL)
game_date = models.DateField(verbose_name='Game Date')
forms.py
class GameForm(ModelForm):
class Meta:
model = Game
fields = (
'location',
'game',
'game_date'
)
I'm pretty sure that the problem is in my views.py: I'm not sure that I'm processing the POST request the right way to feed all data to the preview page.
views.py
def form_upload(request):
if request.method == 'GET':
form = GameForm()
else:
# A POST request: Handle Form Upload
form = GameForm(request.POST) # Bind data from request.POST into a GameForm
# If data is valid, proceeds to create a new game and redirect the user
if form.is_valid():
game = form.save()
return render(request, 'games/success.html', {})
return render(request, 'games/form_upload.html', {
'form': form,
})
preview.py
class GameFormPreview(FormPreview):
form_template = 'games/form_upload.html'
preview_template = 'games/preview.html'
def done(self, request, cleaned_data):
# Do something with the cleaned_data, then redirect
# to a "success" page.
return HttpResponseRedirect('/games/success')
form_upload.html
...
<form method="post">
{% csrf_token %}
<ul><li>{{ form.as_p }}</li></ul>
<button type="submit">Preview your post</button>
</form>
...
preview.html
{% load humanize %}
...
<h1>Preview your submission</h1>
<div>
<p>Location: {{ form.data.location }}</p>
<p>Game Date: {{ form.data.game_date|date:"l, F d, Y" }}</p>
<p>Game Type: {{ form.data.get_game_display }}</p>
</div>
<div>
<form action="{% url 'form_upload' %}" method="post">
{% csrf_token %}
{% for field in form %}
{{ field.as_hidden }}
{% endfor %}
<input type="hidden" name="{{ stage_field }}" value="2" />
<input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" />
<!-- Submit button -->
<button type="submit">Submit your post</button>
<!-- Go back button -->
<button type="submit">
<a href="{% url 'form_upload' %}"
onClick="history.go(-1);return false;" >
Go back and edit your post
</a>
</button>
</div>
</form>
</div>
...
Two issues
Essentially, I'm having these two issues:
String values for choices are not displayed. If I use the get_FOO_display() method in my preview.html template, it returns blank. However, if I use this in a page after the post has been submitted, it displays properly.
The humanize date filter doesn't work. If I apply a humanize filter ({{ form.data.game_date|date:"l, F d, Y" }}) in preview.html, it also displays blank. Again, this works for submitted posts.
My question essentially is: what's the right way to use the FormPreview here?
form.data does not have get_FOO_display attributes. When you access {{ form.data.get_game_display }} in the template, it fails silently and doesn't display anything.
The get_FOO_display are methods of the instance, so try this instead.
{{ form.instance.get_game_display }}
Wherever possible you should access data from form.cleaned_data (which is validated and 'cleaned') instead of form.data, which is the raw data submitted to the form.
The filters don't work with form.data.game_date because it's a raw string. They should work with form.cleaned_data.game_date, which has been converted to a python date object.
Finally, you haven't implemented anything in your done method, you've just copied the comment from the docs. You could create a new game using cleaned_data as follows:
def done(self, request, cleaned_data):
game = Game.objects.create(**cleaned_data)
return HttpResponseRedirect('/games/success')
Related
[Base Update View details][1]
[1]: http://ccbv.co.uk/projects/Django/3.0/django.views.generic.edit/BaseUpdateView/
I am trying to have a Update form(updateview) in in my detailView template which updates one of the foreign key object of my detailview's object. I am getting output as expected and logics are working completely fine. I am using updateviewform as popup form but previous data is not getting prepopulated in the form.
Views.py file
class EducationUpdateView(LoginRequiredMixin,UpdateView):
model = Education
form_class= EducationCreateForm
class PortfolioDetailedView(DetailView):
model = Portfolio
success_url = 'portfolio:index'
def get_context_data(self, **kwargs):
context = super(PortfolioDetailedView, self).get_context_data(**kwargs)
context['education_form'] = EducationCreateForm()
return context
template
here portfolio is the detailsView object and education is its foreign key
{% for item in portfolio.education.all %} <form method="POST" class="form-group" action="{%url 'portfolio:eduupdate' pk=item.pk %}"
{% csrf_token %}
<div class="form-row">
{% bootstrap_form form %}
</div>
<input type="Submit" class="btn btn-primary" value="Submit"> </form>
pasting this for an idea this is not complete code.
The only problem is data is not getting prepopulated into update view form
I have a table with atrributes, and I'm displaying each attribute as a checkbox in html view. I want to show them in different pages, but I don't want to make different functions for each category. Is there an efficient way to do so? Here is what I tried so far.
def questions(request):
# start session page for the user to test
questions = Attribute.objects.all()
realistic = Attribute.objects.filter(holland_code=1)
investigative = Attribute.objects.filter(holland_code=2)
artistic = Attribute.objects.filter(holland_code=3)
social = Attribute.objects.filter(holland_code=4)
enterprising = Attribute.objects.filter(holland_code=5)
conventional = Attribute.objects.filter(holland_code=6)
left = [realistic, investigative, artistic, social, enterprising, conventional]
for attribute in left:
# get all the values form the form submitted
if request.method == "POST":
# THIS WILL GET ALL THE RECOMMENDAITONS
rAttributes = request.POST.getlist('realistic')
print(rAttributes)
return render(request, "main/questions.html", {"questions": attribute})
context = {
"questions": realistic,
}
return render(request, 'main/questions.html', context)
This is my html template to display the checkboxes
<form action="" method="POST">
{% csrf_token %}
<div class="form-check">
{% for question in realistic %}
<input type="checkbox" class="form-check-input" id="exampleCheck1" name="realistics" value="{{ question.attribute_name }}">
<label class="form-check-label" for="exampleCheck1">{{ question.attribute_name }}</label>
<br>
{% endfor %}
</div>
<input type="submit" class="btn btn-danger" value="Next">
</form>
Just add the form to the base template and extend that template so it'll be available everywhere.
or you could create a class based view and add a post request
class InsterViewNameHere(View):
def post(request, self, id=None, *args, **kwargs):
#form logic and context here
context = {}
return render(request, 'template.html', context)
To not have to repeat the same post function for every view or class-based-view, you could create a Mixin view
class ObjectnameMixin(object):
model = ClassModel
form = formname
def post_form(self):
return form
I have struggled with this problem for a while so I appreciate any help, however vague.
Django 2.0.1: The "required" setting that Django uses for validating whether a field is valid works fine if I input:
{{ client_primary_sector }} in to the applicable html file with the "required" setting chosen via the data model (blank=False) or via forms.py (attrs={"required": "required"}). However, the "required" setting fails when I use for loops to produce radio buttons.
See below for a working and broken example.
models.py:.
class SurveyInstance(models.Model):
client_primary_sector = models.CharField(choices=PRIMARY_SECTOR, null=True, default='no_selection', blank=False, max_length=100)
Please note from above the `default='no_selection', which is not in the PRIMARY_SECTOR choices and isn't rendered as an option to the user. This forces the user to select before data is saved (I have confirmed it works).
forms.py
class ClientProfileForm(ModelForm):
class Meta:
model = SurveyInstance
fields = ('client_primary_sector',)
widgets = {'client_primary_sector': forms.RadioSelect(choices=PRIMARY_SECTOR, attrs={"required": "required"}),
}
views.py
def client_profile_edit(request, pk):
# get the record details from the database using the primary key
survey_inst = get_object_or_404(SurveyInstance, pk=pk)
# if details submitted by user
if request.method == "POST":
# get information from the posted form
form = ClientProfileForm(request.POST, instance=survey_inst)
if form.is_valid():
survey_inst = form.save()
# redirect to Next view:
return redirect('questionnaire:business-process-management', pk=survey_inst.pk)
else:
# Retrieve existing data
form = ClientProfileForm(instance=survey_inst)
return render(request, 'questionnaire/client_profile.html', {'form': form})
client_profile.html
<!-- this works: -->
<!-- <div class="radio_3_cols">
{{ form.client_primary_sector }}
</div> -->
<!-- this doesn't: -->
{% for choice in form.client_primary_sector %}
<div class="radio radio-primary radio-inline">
{{ choice.tag }}
<label for='{{ form.client_primary_sector .auto_id }}_{{ forloop.counter0 }}'>{{ choice.choice_label }}</label>
</div>
{% endfor %}
You may wonder why I don't just use the working solution... I would like to be able to use the for loop logic for other situations and so require a solution.
Answered my own question. From the documentation for 2.0:
https://docs.djangoproject.com/en/2.0/ref/forms/widgets/#radioselect
The correct syntax is:
{% for radio in form.client_profile %}
<label for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
<span class="radio">{{ radio.tag }}</span>
</label>
{% endfor %}
Not whatever I found before. Confirmed as working. Hoorah!
When placing 2 forms in a view using the form|crispy filter and this answer to handle 2 forms in a single view: Proper way to handle multiple forms on one page in Django I am getting this error.
views.py:
def test_form(request):
if not request.user.is_authenticated():
return redirect(settings.LOGIN_URL)
title = 'test form'
row_control_form = RowControlForm(request.POST or None)
entry_form = EntryForm(request.POST or None)
context = {
'title': title,
'row_control_form': row_control_form,
'entry_form': entry_form,
}
if 'row_control_submit' in request.POST:
if row_control_form.is_valid():
row_control_form.save()
if 'entry_submit' in request.POST:
if entry_form.is_valid():
entry_form.save()
return render(request, "timesheet/test_form.html", context)
forms.py
class RowControlForm(forms.ModelForm):
class Meta:
model = RowControl
fields = ['month_control_record', 'department', 'activity', 'notes']
def clean(self):
cleaned_data = self.cleaned_data
# Ensures row is unique
try:
RowControl.objects.get(month_control_record=cleaned_data['month_control_record'],
department=cleaned_data['department'],
activity=cleaned_data['activity'],
notes=cleaned_data['notes'])
except RowControl.DoesNotExist:
pass
else:
raise ValidationError('This row already exists')
# Always return cleaned data
return cleaned_data
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['row_control', 'date', 'hours']
def clean(self):
cleaned_data = self.cleaned_data
# Ensures data is unique (only 1 hours entry for each date and row_control)
try:
Entry.objects.get(row_control=cleaned_data['row_control'],
date=cleaned_data['date'])
except Entry.DoesNotExist:
pass
else:
raise ValidationError('This entry already exists')
# Always return cleaned data
return cleaned_data
test_form.html
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="col-md-6 col-md-offset-3">
<h1 class="page-header"> Form Test </h1>
<form method="POST" action="{{ request.path }}">
{% csrf_token %}
{{ row_control_form|crispy }}
<button class="btn btn-primary" type="submit" value="Submit" name="row_control_submit" ><i class="fa fa-lg fa-floppy-o"></i> Save</button> </form>
</br>
</div>
<div class="col-md-6 col-md-offset-3">
<h1 class="page-header"> Form Test </h1>
<form method="POST" action="{{ request.path }}">
{% csrf_token %}
{{ entry_form|crispy }}
<button class="btn btn-primary" type="submit" value="Submit" name="entry_submit" ><i class="fa fa-lg fa-floppy-o"></i> Save</button> </form>
</br>
</div>
{% endblock %}
To provide context to the error:
Line 42 of forms.py is:
Entry.objects.get(row_control=cleaned_data['row_control'],
EDIT: Further investigation has shown that the issue is that both form validations are being run no matter which submit button is pressed, the request.POST when submitting valid data for the RowControlForm is:
<QueryDict: {'csrfmiddlewaretoken': ['HffmmbI31Oe0tItYDfYC4MoULQHL0KvF'], 'notes': ['Cool'], 'row_control_submit': ['Submit'], 'month_control_record': ['1'], 'department': ['1'], 'activity': ['1']}>
Therefore entry_submit is not in the request.POST and that validation should not run yet it is?
Firstly, you need to fix this line of your form's clean method
def clean(self):
...
Entry.objects.get(row_control=cleaned_data['row_control'],
You can't assume that row_control will be in the cleaned_data. You either need to add a check if 'row_control' in cleaned_data or catch the KeyError, then update the rest of the method appropriately. You should fix this, even though you didn't see this error until you put multiple forms on one page. It shouldn't be possible to cause a 500 server error by leaving a value out of a POST request. Users could do this even if there is only one form on the page.
Validation is running for both forms, because you are instantiating both forms with the post data, regardless of which submit button was pressed.
row_control_form = RowControlForm(request.POST or None)
entry_form = EntryForm(request.POST or None)
You should only use the POST data for the form you wish to submit.
row_control_form = RowControlForm()
entry_form = EntryForm()
if 'row_control_submit' in request.POST:
row_control_form = RowControlForm(request.POST)
if row_control_form.is_valid():
if 'entry_submit' in request.POST:
entry_form = EntryForm(request.POST)
if entry_form.is_valid():
entry_form.save()
Finally, it's good practice to redirect the user once they have successfully submitted a valid form.
I am trying to create a form that is a list of cars with one field being a BooleanField. I want this to appear as a form with the BooleanField being a checkbox. If the user checks this, then the BooleanField will be set = True and something will happen when a POST occurs and the user is redirected to the next page.
model.py:
class Car(models.Model):
year = models.IntegerField()
make = models.CharField(max_length=30)
model = models.CharField(max_length=30)
send = models.BooleanField(default=False)
currenly the email.html looks like this:
<form action="" method="post">{% csrf_token %}
{% for car in object_list %}
<input type="checkbox" name="car" id="car{{ forloop.counter }}" value="{{ car.id }}">
<label for="car{{ forloop.counter }}">{{ car.year }} {{ car.make }} {{ car.model }}</label><br>
{% endfor %}
<input type="submit" value="Preview">
</form>
views.py
class Email(ListView):
model = Car
template_name = 'cars/email.html'
Suggestions?
I ended up adding a post() function to the ListView which processes the form data, but I am having trouble redirecting without a valid HttpResponse object and am getting an error when one of the boxes isn't checked in the form. Here is the additional post() code that I added for the time being:
def post(self, request, *args, **kwargs):
cars = Car.objects.all() # initially reset "self.send" field == False
for i in range(len(cars)):
cars[i].send = False
cars[i].save()
cars = Car.objects.filter(id__in=request.POST.getlist('car'))
for i in cars:
i.send = True
i.save()
return HttpResponseRedirect(reverse('cars:email_preview'))
Any suggestions on how to make the form re-render with an error msg if no boxes are checked?
Thanks