I'm creating a survey application that displays the survey question and choices and allows the user to pick a choice through the Flask-WTForms package. The form uses a RadioField and seems to fail form.validate() when populating the choices attribute dynamically.
When I manually enter in the choices as such:
class SurveyAnswerForm(FlaskForm):
answers = RadioField('Answers',
coerce=str,
choices=[('18-25', '18-25'), ('26-35', '26-35')])
form.validate() returns True and there are no errors in form.error.
When I decide to populate the choices attribute dynamically (see below), form.validate() returns False and form.error returns:
{'answers': ['Not a valid choice']}.
I've been working at this for hours and am not sure why form.validate() returns False.
forms.py:
from flask_wtf import FlaskForm
from wtforms import RadioField
class SurveyAnswerForm(FlaskForm):
answers = RadioField('Answers',
coerce=str,
choices=[])
app.py:
#app.route('/survey/<int:survey_id>/questions', methods=['GET', 'POST'])
def survey_questions(survey_id):
survey = Survey.query.filter_by(id=survey_id).first()
page = request.args.get('page', 1, type=int)
questions = SurveyQuestion.query.filter_by(survey_id=survey_id)\
.order_by(SurveyQuestion.id)\
.paginate(page, 1, True)
for question in questions.items:
question_id = question.id
choices = QuestionChoices.query\
.join(SurveyQuestion,
and_(QuestionChoices.question_id==question_id,
SurveyQuestion.survey_id==survey_id)).all()
form = SurveyAnswerForm(request.form)
form.answers.choices = [(choice.choice, choice.choice)\
for choice in choices]
if request.method =='POST' and form.validate():
print('Successful POST')
next_url = url_for('survey_questions', survey_id=survey.id,
page=questions.next_num)\
if questions.has_next else None
prev_url = url_for('survey_questions', survey_id=survey.id,
page=questions.prev_num)\
if questions.has_prev else None
return render_template('survey_question.html',
survey=survey,
questions=questions.items,
choices=choices,
form=form,
next_url=next_url, prev_url=prev_url)
survey_question.html:
{% extends "layout.html" %}
{% block body %}
<h2>{{ survey.survey_title }}</h2>
{% for question in questions %}
<h3>{{ question.question }}</h3>
{% endfor %}
<form action="{{ next_url }}" method="POST">
{{ form.csrf_token }}
{{ form.answers(name='answer') }}
{% if prev_url %}
Back
{% endif %}
{% if next_url %}
<input type="submit" value="Continue">
{% else %}
Finish
{% endif %}
</form>
{% endblock %}
The problem was submitting a POST request with pagination. if the current link is /survey/1/question?page=2 the form will submit to /submit/1/question?page=3. To remedy this, I just created a separate route for submission and handled logic there.
Related
I am creating a movie review website. In it, I want to be able to allow a User to make one comment on one movie and then Update or Delete that comment. But I am only able to implement POST right now. How do I change the view, html or model?
Question to ask
How can I keep the comments posted by a user at the top of the comment list so that they can be updated and deleted?
An example of what we would like to implement is Rotten Tomatoes.
Models.py:
class Comment_movie(models.Model):
comment = models.TextField(max_length=1000)
stars = models.FloatField(
blank=False,
null=False,
default=0,
validators=[MinValueValidator(0.0),
MaxValueValidator(10.0)]
)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
created_at = models.DateTimeField(default=datetime.now)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('user', 'movie')
indexes = [
models.Index(fields=['user', 'movie']),
]
views.py:
def view_movie_detail(request, movie_id):
if not(Movie.objects.filter(id=movie_id)):
Movie(id = movie_id).save()
movie = Movie.objects.get(id=movie_id)
if request.method == "POST":
form = Comment_movie_CreateForm(request.POST)
if form.is_valid():
Comment_movie(
comment = form.cleaned_data['comment'],
user = request.user,
stars = form.cleaned_data['stars'],
movie = movie
).save()
return redirect('view_movie_detail', movie_id=movie_id)
else:
form = Comment_movie_CreateForm()
data = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}?api_key={TMDB_API_KEY}&language=en-US")
recommendations = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}/recommendations?api_key={TMDB_API_KEY}&language=en-US")
comments = reversed(Comment_movie.objects.filter(movie_id=movie_id))
average = movie.average_stars()
context = {
"data": data.json(),
"recommendations": recommendations.json(),
"type": "movie",
"comments": comments,
"average" : average,
"form": form,
}
return render(request, "Movie/movie_detail.html", context)
movie.html:
<h2>Comments</h2>
{% if form.errors %}
<div class = "error_list">
{% for errors in form.errors.values %}
{% for error in errors %}
{{ error }}<br>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<button type="submit">Post Comment</button>
</form>
{% endif %}
<hr>
Looks like you want to do multiple actions in one view. One form for each action and a template field to differentiate actions would be a solution. In this specific case, 'create' action and 'update' action can be automatically determined if we take advantage of unique_together.
from django.shortcuts import get_object_or_404
def view_movie_detail(request, movie_id):
# It makes little sense you create a movie with just an id attr
# So I use get_object_or_404 instead
movie = get_object_or_404(Movie, id=movie_id)
try:
comment_movie = Comment_movie.objects.get(user=request.user, movie=movie)
except Comment_movie.DoesNotExist:
comment_movie = None
if request.method == 'POST':
if request.POST.get('action') == 'delete':
comment_movie.delete()
return redirect('view_movie_detail', movie_id=movie_id)
else:
form = Comment_movie_CreateForm(request.POST, instance=comment_movie)
if form.is_valid():
form.save()
return redirect('view_movie_detail', movie_id=movie_id)
else:
form = Comment_movie_CreateForm(instance=comment_movie)
# Put your view logic outside of the conditional expression.
# Otherwise your code breaks when the form validates to False
data = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}?api_key={TMDB_API_KEY}&language=en-US")
recommendations = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}/recommendations?api_key={TMDB_API_KEY}&language=en-US")
comments = reversed(Comment_movie.objects.filter(movie_id=movie_id).exclude(user=request.user))
average = movie.average_stars()
context = {
"data": data.json(),
"recommendations": recommendations.json(),
"type": "movie",
"comments": comments,
"average" : average,
"form": form,
"comment_movie": comment_movie, # NOTE add the comment to context
}
return render(request, "Movie/movie_detail.html", context)
Note instance=coment_movie will make form use instance attribute when rendering in template.
And in your templates, render all three forms, and add ‘action’ to each form. One good place would be the submit button.
<h2>Comments</h2>
{% if form.errors %}
<div class = "error_list">
{% for errors in form.errors.values %}
{% for error in errors %}
{{ error }}<br>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
{% if comment_movie %}
<button type="submit">Edit Comment</button>
<button type="submit" name="action" value="delete">Delete Comment</button>
{% else %}
<button type="submit">Post Comment</button>
{% endif %}
</form>
{% endif %}
<hr>
{% for comment in comments %}
<div>{{ comment.comment }}</div>
{% endfor %}
Check out django-multi-form-view. This module does not fit your question perfectly, but shares some basic ideas.
Note two addtional submit buttons in template. They are rendered only if comment is not None, which means user has made comment before. The second button coresponds to action='delete'
To your question: Render the form first, and render the rest comments after the form such that user comment is always at top.
I have following scenario.
User fills out a form
If the user clicks the "continue" button and the form is valid the user will be redirected to a summary view
In the summary view the user checks the input again. He can either continue or go back.
If he continues the data will be saved in the database, if he goes back he can edit the form.
Since in step 4 the user is at the view summary I have to redirect him to the home view. I don´t want the user to fill out the entire form again, the previously entered data should be autofilled if he goes back.
Something special: I am using django-tagify2 for one input in the form to get tags rather then text. If the user goes back the tags should be rendered correctly in the tagify specific form.
So here are my files:
home.html
{% extends "messenger/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="message-container">
<form method="POST" autocomplete="off">
{% csrf_token %}
{{ form|crispy }}
<button name="sendmessage" class="btn btn-outline-info" type="submit">Continue</button>
</form>
</div>
{% endblock content %}
summary.html
{% extends "messenger/base.html" %}
{% block content %}
<h4>Zusammenfassung</h4>
<p><b>Empfänger: </b>{{ receiver }}</p>
<br>
<p><b>Betreff: </b>{{ subject }}</p>
<br>
<p><b>Nachricht: </b>{{ message }}</p>
<button>Edit</button>
<button>Continue</button>
{% endblock content %}
home view
#login_required(login_url='login')
def home(request):
if request.method == 'POST' and 'sendmessage' in request.POST:
message_form = MessageForm(request.POST)
if message_form.is_valid():
receiver_list = message_form['receiver'].value().split(';')
subject = message_form['subject'].value()
message = message_form['textmessage'].value()
#create sessions and send data to next view
session_receiver = receiver_list
request.session['session_receiver'] = session_receiver
session_subject = subject
request.session['session_subject'] = session_subject
session_message = message
request.session['session_message'] = session_message
return redirect('summary')
else:
message_form = MessageForm()
return render(request, 'messenger/home.html', {'form': message_form})
summary view
def summary(request):
receiver = request.session.get('session_receiver')
subject = request.session.get('session_subject')
message = request.session.get('session_message')
return render(request, 'messenger/summary.html', {'receiver':receiver,
'subject':subject,
'message':message})
So what is the best way to do this?
Can I use the session variables to set the fields in the form?
I don´t want to change the logic in the app. I want a home/summary/success view/template where I can loop as long is I want between home and summary until the user is happy with his entered form data
How about checking request.session when there is get request to home view? Then you can bind message_form = MessageForm() to session data.
You can check out htmx and django-htmx. You can do what you want easily without session by swapping HTML with context.
I played around with the session values and views and finally got a way to redirect to other views with prefilled form fields based on session values.
#login_required(login_url='login')
def home(request):
if request.method == 'POST' and 'sendmessage' in request.POST:
message_form = MessageForm(request.POST)
if message_form.is_valid():
ad_group = message_form['ad_group'].value().split(';')
ad_user = message_form['ad_user'].value().split(';')
subject = message_form['subject'].value()
message = message_form['textmessage'].value()
#create sessions and send data to next view
session_ad_group = ad_group
request.session['session_ad_group'] = session_ad_group
session_ad_user = ad_user
request.session['session_ad_user'] = session_ad_user
session_subject = subject
request.session['session_subject'] = session_subject
session_message = message
request.session['session_message'] = session_message
return redirect('summary')
else:
if request.session.get('session_subject'):
message_form = MessageForm(initial={'ad_group': request.session.get('session_ad_group'),
'ad_user': request.session.get('session_ad_user'),
'subject': request.session.get('session_subject'),
'textmessage': request.session.get('session_message')})
return render(request, 'messenger/home.html', {'form': message_form})
else:
message_form = MessageForm()
return render(request, 'messenger/home.html', {'form': message_form})
def summary(request):
ad_group = request.session.get('session_ad_group')
ad_user = request.session.get('session_ad_user')
subject = request.session.get('session_subject')
message = request.session.get('session_message')
if request.method == 'POST' and 'edit' in request.POST:
message_form = MessageForm(initial={'ad_group':ad_group, 'ad_user': ad_user,
'subject':subject, 'textmessage':message})
return render(request, 'messenger/home.html', {'form': message_form})
return render(request, 'messenger/summary.html', {'ad_group':ad_group,
'ad_user': ad_user,
'subject':subject,
'message':message})
Template
{% extends "messenger/base.html" %}
{% block content %}
<h2>Zusammenfassung</h2>
<div class="border-top pt-3">
<p><b>AD-Gruppe: </b>{{ ad_group }}</p>
<p><b>AD-User: </b>{{ ad_user }}</p>
<br>
<p><b>Betreff: </b>{{ subject }}</p>
<br>
<p><b>Nachricht: </b>{{ message }}</p>
<div class="buttoncontainer">
<form name="edit" action="" method="post">
{% csrf_token %}
<button class="btn edit_btn" formaction="{% url 'messenger-home' %}">Zurück</button>
</form>
<form name="senden" action="" method="post">
{% csrf_token %}
<button class="btn continue_btn" formaction="{% url 'send_messages' %}">Nachricht senden</button>
</form>
</div>
</div>
{% endblock content %}
I have multiple form on a page in modals: 1-st to create a new user address and n-forms with addresses which user created (created with loop). When i enter invalid data in fields with validators (e.g. datarequired), i have error messages in each form.
Here is the field render example which i use in every form:
{{ address_form.street.label(class_="form-label", for="InputStreet") }}
{{ address_form.street(class_="form-control", id="InputStreet") }}
{% for error in address_form.street.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
Part of the code from view.py:
#bp.route('/profile/address', methods=['GET', 'POST'])
#login_required
def address():
address_form = AddressForm()
if address_form.submit_address.data and address_form.validate():
address_to_add = Address(
street=address_form.street.data,
house=address_form.house.data,
building=address_form.building.data,
entrance=address_form.entrance.data,
floor=address_form.floor.data,
apartment=address_form.apartment.data,
additional_info=address_form.additional_info.data,
user=current_user)
db.session.add(address_to_add)
db.session.commit()
return redirect(url_for('profile.address'))
if address_form.edit_address.data and address_form.validate():
address_to_edit = Address.query.get(address_form.address_id.data) # Here is data from hidden field
# Editing data in DB
db.session.commit()
return redirect(url_for('profile.address'))
return render_template('profile/address.html', title='Адрес доставки', address_form=address_form)
Forms work fine with adding, editing and deleting data, but work incorrect with validation errors.
I think i need one more condition in if statenent related with hidden field or change something in my html file.
I've tried add an action attr in form like:
<form action="{{ url_for('profile.address', form_id=address.id) }}" method="post" novalidate>
And smth like this in view func but it doesn't work:
form_id = request.args.get('form_id', type=int)
if address_form.edit_address.data and address_form.validate() and form_id == address_form.address_id.data:
pass
Finally i found a very bad solution:
address_form = AddressForm()
form_id = request.args.get('form_id', 0, type=int)
For main form form_id always is 0.
Form tag for main form:
<form action="{{ url_for('profile.address', form_id=form_id) }}" method="post" novalidate>
And a field render for main form:
{{ address_form.street.label(class_="form-label", for="InputStreet") }}
{{ address_form.street(class_="form-control", id="InputStreet") }}
{% if form_id == 0 %}
{% for error in address_form.street.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{% endif %}
If someone has a better solution about my problem it'd good. Now it's time to learn some JS and solve this problem with AJAX.
I am tring to sent email with django using gmail smtp server i write settings in setting.py.here is my other code but I am getting AttributeError at /share/4 'str' object has no attribute 'get' ? please help me to solve that error.
**forms.py**
from django import forms
class EmailPostForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
to = forms.EmailField()
comment = forms.CharField(widget=forms.Textarea, required=False)
views.py
def share_email(request, id):
post = get_object_or_404(Post, id=id)
sent = False
if request.method == 'POST':
form = EmailPostForm(data=request.method)
if form.is_valid():
cd = form.cleaned_data
post_url =
request.build_absolute_uri(post.get_absolute_url())
subject = '{} ({}) recommend you reading "{}"'.format(cd['name'], cd['email'], post.title)
message = 'Read "{}" at {}\n\n{}\'s comments: {}'.format(post.title, post_url, cd['name'], cd['comment'])
send_mail(subject, message, 'admin#gmail.com', cd['to'])
sent = True
else:
form = EmailPostForm()
return render(request, 'blog/post/share_post.html', {'post': post, 'form': form, 'sent': sent})
url.py
urlpatterns = [path('share/<int:id>', views.share_email, name='share_post'),]
share_post.html
{% extends 'blog/base.html' %}
{% block title %}
Share Post
{% endblock %}
{% block content %}
{% if sent %}
<h2>E-mail successfully sent</h2>
<p>{{ post.title }} is successfully dent by email</p>
{% else %}
<h2>Share {{ post.title }} by email</h2>
<form action="{% url 'blog:share_post' post.id %}" method="post">
{{ form.as_p }}
{% csrf_token %}
<input type="submit" value="Send Email">
</form>
{% endif %}
{% endblock %}
Here:
form = EmailPostForm(data=request.method)
you want request.POST, not request.method.
As a side note: a successful post should be followed by a redirect (to prevent page reload to repost the same data). You can use the contrib.messages app to set a message in session, that will then be displayed on the next page.
I am trying to do a basic search feature but I am having a small issue. When I go to the template that has the search form, it is displaying all the items before I even try to search. Is there a way to show a blank template until the user has put in a search term and hit the search button?
Example:
[Search field][Button]
1
2
3
etc
views.py
def view_player_home(request):
if request.method == 'GET':
form = searchPlayerForm(request.GET)
if form.is_valid():
string = form.cleaned_data.get('text')
players = Player.objects.filter(Q(first_name__icontains = string)|Q(last_name__icontains = string))
return render_to_response('player/player.html', {'form': form, 'players':players}, context_instance=RequestContext(request))
else:
form = searchPlayerForm()
return render_to_response('player/player.html', {'form': form}, context_instance=RequestContext(request))
forms.py
class searchPlayerForm(forms.Form):
text = forms.CharField(label = "Search")
def __init__(self, *args, **kwargs):
super(searchPlayerForm, self).__init__(*args, **kwargs)
self.fields['text'].required = False
template
{% extends "base.html" %}
{% block content %}
<h5>Find Player</h5>
<form method="GET" action="">
{% csrf_token %}
{{ form.as_table }}
<input type="submit" value="Submit"/>
</form>
{% if players %}
{% for p in players %}
{{ p.first_name }} {{ p.last_name }}
{% endfor %}
{% else %}
No Players
{% endif %}
{% endblock %}
One change should do it:
if request.method == 'GET':
should be
if request.GET:
The underlying issue is that your request method is always GET, so you never go into the else block or to the bottom of the function.
Another option is to explicitly look for a term in the GET data
if request.GET and 'text' in request.GET:
# do query / processing
or even don't allow blanks
if request.GET and 'text' in request.GET and request.GET['text'] != '':
# do query / processing
This work easily if you only have one field or are checking if options in a form have certain values.
When doing a lot of fields, I like to do a named submit button so that I can check is it's been hit, and then do the if statements checking for the button name.