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.
Related
I have the following models defined:
class Item(models.Model):
rfid_tag = models.CharField()
asset = models.OneToOneField('Assets', default=None, null=True,
on_delete=models.SET_DEFAULT,)
date = models.DateTimeField(name='timestamp',
auto_now_add=True,)
...
class Assets(models.Model):
id = models.AutoField(db_column='Id', primary_key=True)
assettag = models.CharField(db_column='AssetTag', unique=True, max_length=10)
assettype = models.CharField(db_column='AssetType', max_length=150)
...
class Meta:
managed = False
db_table = 'Assets'
ordering = ['assettag']
def __str__(self):
return f"{self.assettag}"
def __unicode__(self):
return f"{self.assettag}"
For which I have created the following form and formset:
class ItemDeleteForm(forms.ModelForm):
asset = forms.CharField(required=True,
help_text= "Item asset tag",
max_length=16,
label="AssetTag",
disabled=True,
)
delete = forms.BooleanField(required=False,
label="Delete",
help_text='Check this box to delete the corresponding item',
)
class Meta:
model = Item
fields = ['asset']
ItemDeleteMultiple = forms.modelformset_factory(model=Item,
form=ItemDeleteForm,
extra=0,
)
managed by the view:
class DeleteMultipleView(generic.FormView):
template_name = '*some html file*'
form_class = ItemDeleteMultiple
success_url = reverse_lazy('*app_name:url_name*')
def form_valid(self, form):
return super().form_valid(form)
And rendered in the template:
{% extends "pages/base.html" %}
{% block title %}
<title>Delete Multiple</title>
{% endblock %}
{% block content %}
<h1>Delete Multiple Items</h1>
<br>
<form class="ManualForm" action ="." method="POST"> {% csrf_token %}
{{ form.management_form }}
<table border="2">
<tr><th colspan="3" scope="row">Select Items to Delete</th></tr>
{% for item_form in form %}
<tr>
<td><label for="{{ item_form.asset.id_for_label }}">AssetTag {{forloop.counter}}:</label>
{% if item_form.non_field_errors %}
{{ item_form.non_field_errors }}
{% endif %}
{% if item_form.asset.errors %}
{{item_form.asset.errors}}
{% endif %}
</td>
<td>{{item_form.asset}}</td>
<td>{{item_form.delete}}
{% if item_form.delete.errors %}
{{item_form.delete.errors}}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<br>
<input class = "btn btn-success" type="submit" value="Delete Selected" />
Cancel
</form>
<form class="AutoForm" action ="." method="POST"> {% csrf_token %}
{{form.as_table}}
<input class = "btn btn-success" type="submit" value="Delete Selected" />
Cancel
</form>
{% endblock %}
When I submit AutoForm, everything is great. It takes me to app_name:url_name, but if I sumbit ManualForm I don't get redirected. It will simply clear all data and reload the form page with empty fields.
The HTTP POST response status code for AutoForm is 302, while for ManualForm is 200.
I don't understand how the template could influence the behavior of the url redirection. What am I doing wrong in the manual rendering of the formset?
It seems that adding:
{% for field in item_form.hidden_fields %}
{{field}}
{% endfor %}
under {% for item_form in form %} will solve the issue.
I didn't understand very well from the docs:
Looping over hidden and visible fields
If you’re manually laying out a form in a template, as opposed to
relying on Django’s default form layout, you might want to treat
< input type="hidden"> fields differently from non-hidden fields. For
example, because hidden fields don’t display anything, putting error
messages “next to” the field could cause confusion for your users – so
errors for those fields should be handled differently.
I just thought this is about errors, so I didn't care. But one of the first thing it says about forms is this:
As an example, the login form for the Django admin contains several
< input> elements: one of type="text" for the username, one of
type="password" for the password, and one of type="submit" for the
“Log in” button. It also contains some hidden text fields that the
user doesn’t see, which Django uses to determine what to do next.
It also tells the browser that the form data should be sent to the URL
specified in the < form>’s action attribute - /admin/ - and that it
should be sent using the HTTP mechanism specified by the method
attribute - post.
Maybe it will help someone else.
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.
What am I doing ?
I'm training on a simple application where one can order a pizza and select his toppings, once the form submitted it shows the submitted queries in the template file.
What is the problem?
I'm having a really hard time showing the checked checkboxes from the form on the template file.
Here are my files :
models.py
class PickedDatas(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
class Picked(models.Model):
name = models.CharField(max_length=255)
picked = models.ManyToManyField(PickedDatas, blank=True)
forms.py
class CustomChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return mark_safe('%s' % (obj.name))
class SomeForm(forms.ModelForm):
class Meta:
model = Picked
fields = ['name', 'picked']
picked = CustomChoiceField(queryset=PickedDatas.objects.all(), widget=forms.CheckboxSelectMultiple())
views.py
def some_view(request):
if request.method == 'POST':
form = SomeForm(request.POST)
if form.is_valid():
...
else:
form = SomeForm
return render(request, 'features.html', {'form':form, 'picked':Picked.objects.all()})
As for the template file, I'm using the for loop to show Picked models datas.
How can I achieve what I am trying to do ?
EDIT
here is the template file features.html
<h2>Enter your name and choose your pizza toppings</h2>
<form method='post'>
{% csrf_token %}
{{ form.as_p }}
<input type='submit' value='submit'>
</form>
{% for p in picked %}
<h2>Pizza For : <strong>{{ p.name }}</strong></h2>
<p>{{ p.picked }}</p>
{% endfor %}
it gives me this for {{ p.picked }} : pizza.PickedDatas.None
Picked.picked is a many to many field, so you need to loop through the options:
{% for picked in picked %}<!-- Note renamed variable to prevent clash with inner loop -->
<h2>Pizza For : <strong>{{ picked.name }}</strong></h2>
<p>{% for p in picked.picked.all %}{{ p }}{% endfor %}</p>
{% endfor %}
Not sure what I did wrong. I'm making a commenting system that users can post comment on a post and be able to reply to that comment. Every thing works fine except when user reply to some comment that reply is being shown as a single comment.(and the it's shown as a reply as well) so if I reply to comment "how are you" with a message "good", then I have created two "good". one as the reply. one as the single comment. I've looked around my code, but I have no idea why this is happening. Any help would be highly appreciated. Thank you/.Here's my code.
views.py
def post(request, slug):
user = get_object_or_404(User,username__iexact=request.user)
try:
profile = MyProfile.objects.get(user_id=request.user.id)
# if it's a OneToOne field, you can do:
# profile = request.user.myprofile
except MyProfile.DoesNotExist:
profile = None
post = get_object_or_404(Post, slug=slug)
post.views += 1 # increment the number of views
post.save() # and save it
path = request.get_full_path()
comments = Comment.objects.filter(path=path)
#comments = post.comment_set.all()
comment_form = CommentForm(request.POST or None)
if comment_form.is_valid():
parent_id = request.POST.get('parent_id')
parent_comment = None
if parent_id is not None:
try:
parent_comment = Comment.objects.get(id=parent_id)
except:
parent_comment = None
comment_text = comment_form.cleaned_data['comment']
new_comment = Comment.objects.create_comment(
user=MyProfile.objects.get(user=request.user),
path=request.get_full_path(),
text=comment_text,
post = post,
parent = parent_comment
)
comment_form = CommentForm()
return HttpResponseRedirect(post.get_absolute_url())
for c in comments:
c.get_children()
context_dict = {
'post' :post,
'profile' :profile,
'comments' : comments,
'comment_form':comment_form
}
return render(request, 'main/post.html', context_dict)
models.py
class Comment(models.Model):
def get_children(self):
if self.is_child:
return None
else:
return Comment.objects.filter(parent=self)
post.html
<h1>Comments/Questions</h1>
<form method="POST" action=''>{% csrf_token %}
{{ comment_form.as_p }}
<input type='submit' class='btn btn-default' value='Add comment'/>
</form>
<br/>
<hr/>
<table class='table'>
{% for comment in comments %}
<tr><td>{{ comment.get_comment }}
<br/><small>via {{ comment.user }} | {{ comment.timestamp|timesince }} ago </small>
{% if not comment.is_child %}
<ul>
{% for child in comment.get_children %}
<li>{{ child.get_comment }}
<small>via {{ child.user }}</small>
</li>
{% endfor %}
</ul>
<a href='#' class='reply_btn'>Reply</a>
<div class='reply_comment'>
<form method="POST" action=''>{% csrf_token %}
<input type='hidden' name='parent_id' value='{{ comment.id }}' />
{{ comment_form.as_p }}
<input type='submit' class='btn btn-default' value='Add reply'/>
</form>
</div>
{% endif %}
</td></tr>
{% endfor %}
</table>
</div>
And the comment is displayed in post.html and for that page, view is
def post(request, slug):
user = get_object_or_404(User,username__iexact=request.user)
try:
profile = MyProfile.objects.get(user_id=request.user.id)
# if it's a OneToOne field, you can do:
# profile = request.user.myprofile
except MyProfile.DoesNotExist:
profile = None
post = get_object_or_404(Post, slug=slug)
post.views += 1 # increment the number of views
post.save() # and save it
path = request.get_full_path()
comments = Comment.objects.filter(path=path)
#comments = post.comment_set.all()
comment_form = CommentForm(request.POST or None)
for c in comments:
c.get_children()
context_dict = {
'post' :post,
'profile' :profile,
'comments' : comments,
'comment_form':comment_form
}
return render(request, 'main/post.html', context_dict)
The duplicate comments are just a result of your template.
It is happening because when you loop through all comments, the newly made comment is included in the comments too.
Besides acting as a comment child, it is also a comment object and that is why it is included in commments when you filter() comments with a supplied path and then it is rendered in template as a normal comment object.
For fixing this, you need to use recursive tree. There are available django apps like django-mptt which comes handy in this type of issues.
After integration it into your comments model, you can simply use the {% recursetree %} templatetag to render comments without duplication.
I'm new to Django and was needing some help on a view error i am getting.
I wrote a view that will display a data table of "Groups" if the request method is GET, or display a form to edit a particular "Group" if the request method is POST (item to edit is passed with POST data).
Also, if POST on an existing item, i'd like the form to be pre-populated with the data i already have in the table for that item. I've pretty much got it all down, except when i goto save an edited form, i keep getting this error:
"Cannot set values on a ManyToManyField which specifies an intermediary model"
Any help would be greatly appreciated. Also, I'm new to all this web dev stuff, so if i'm doing something completely silly or am missing a concept, please feel free to flame me accordingly. ;-)
Model
class Alias(models.Model):
def __unicode__(self):
return unicode(self.alias)
alias = models.CharField(max_length=32)
class Octet(models.Model):
def __unicode__(self):
return unicode(self.num)
num = models.IntegerField(max_length=3)
class Group(models.Model):
def __unicode__(self):
return unicode(self.name)
name = models.CharField(max_length=32) #name of the group
id = models.AutoField(primary_key=True) #primary key
octets = models.ManyToManyField(Octet, through='OctetAssignment', blank=True) #not required
aliases = models.ManyToManyField(Alias, through='AliasAssignment', blank=True) #not required
class OctetAssignment(models.Model):
octet = models.ForeignKey(Octet) #octet
group = models.ForeignKey(Group) #group that octet is assigned to
class AliasAssignment(models.Model):
alias = models.ForeignKey(Alias)
group = models.ForeignKey(Group)
View
def index(request):
if request.method == 'GET':
groups = Group.objects.all().order_by('name')
return render_to_response('groups.html',
{ 'groups': groups, },
context_instance = RequestContext(request),
)
elif request.method == "POST":
g = Group.objects.get(id=request.POST['id'])
form = GroupEditForm(instance=g)
return render_to_response('groups.html',
{ 'form': form, },
context_instance = RequestContext(request),
)
def save(request):
if request.method == "POST":
form = GroupEditForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect('/tradedesk/groups/') # Redirect after POST
To make it complete, here is the form template code i'm using that renders the table and edit page.
Template
<h1>Group Information</h1>
{% if groups %}
<table border=1>
{% for group in groups %}
<tr>
<td>{{group.name}}</td>
<td>{% for octet in group.octets.all %} {{octet}} {% endfor %}</td>
<td>{% for alias in group.aliases.all %} {{alias}} {% endfor %}</td>
<td>{{group.analyst}}</td>
</tr>
{% endfor %}
</table>
<br></br>
<form method="post" action="/groups/">{% csrf_token %}
<select name="id" >
{% for group in groups %}
<option value="{{group.id}}">{{group.name}}</option>
{% endfor %}
</select>
<input type="submit" value="Edit">
</form>
{% endif %}
{% if form %}
<form method="post" action="/groups/save/">{% csrf_token %}
{{form}}
<br></br>
<input type="submit" value="Save">
<form>
{% endif %}
</div>
Try to remove the intermediary models OctetAssignment and AliasAssignment. They should be used only when you want to add custom fields to them. Otherwise Django creates them and uses them transparently by itself.