I'm learning Django and I am currently building blog app. I have a problem. I built functionalities to add new posts and to comment to new posts. Now I struggle with creating the possibility to delete comments by users.
I assigned an id (primary key) of given comment to button "delete" which is present in all comments fields in purpose to know which comment I want to delete. But now I don't know how to access to this HTML elements in backend and fetch this id's.
Part of my html file:
{% for comment in comments %}
{% if comment.post == object %}
<div class="article-metadata">
<small>{{ comment.add_date|date:"M/d/Y" }} <b>{{ comment.author }}</b></small>
{% if comment.author == user %}
Delete
{% endif %}
<p>{{ comment.comm_content }}</p>
</div>
{% endif %}
{% endfor %}
My class based view in views.py where I want to touch this id:
class PostDetailView(DetailView):
model = Post
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comments'] = Comment.objects.all()[::-1]
return context
# In this function I want to access my comment id and remove this comment from database
def delete_comment(self):
post_to_delete = Post.objects.get(id=my_comment_id)
del post_to_delete
I know this can be solved somehow using jquery but I don't know javascript and for now I would like to know how to do it using python only. Thanks for any hints.
My models.py file:
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.urls import reverse
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})
class Comment(models.Model):
comm_content = models.TextField()
add_date = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
def __str__(self):
return f"Comment of post {self.post} posted at {self.add_date}."
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.post.pk})
First, add a URL pattern like:
# urls.py
path('comments/delete/<int:pk>/', views.CommentDeleteView.as_view(), name='delete_comment'),
next, you need a view to handle the logic:
# views.py
from django.urls import reverse_lazy
class CommentDeleteView(DeleteView):
model = Comment
def get_success_url(self):
return reverse_lazy('post-detail', kwargs={'pk': self.object.post.pk})
# if you don't want to send POST request, you can use:
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
Django's generic DeleteView will delete the object only if you send a POST request to it, so we will call the post method in get, to make it work with a GET request; Although, generally speaking, it's not a good idea; you should add a confirmation form (like a modal) and send a POST from there.
and finally, for your template:
{% for comment in comments %}
{% if comment.post == object %}
<div class="article-metadata">
<small>{{ comment.add_date|date:"M/d/Y" }} <b>{{ comment.author }}</b></small>
{% if comment.author == user %}
Delete
{% endif %}
<p>{{ comment.comm_content }}</p>
</div>
{% endif %}
{% endfor %}
Related
I am building a Django project where I have an index page that lists all posts. The user can click on the name of a post and this will take them to a detail page with the complete post information (date, content, category). This detail page also has a link that will take the user to a form where they can leave a comment. Once the user clicks submit they are supposed to navigate back to the post detail page and the comment is supposed to be there. The issue I am having right now is that the comment is being automatically assigned to the first post in the index list rather than the post the user had visited (I think this may have something to do with the current default setting in my models, but how else can I get the post id?). How can I make it so that the comment is assigned to its correct post? I have tried everything with the models and views but nothing seems to work. Thank you for your help, I think the solution to this might be simple but I can't find it anywhere.
Here is my relevant models:
class UserPost(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, default=1,
on_delete = models.CASCADE
)
#id = models.AutoField(primary_key=True)
def __str__(self):
"""String for representing the UserPost object."""
return self.title
def get_absolute_url(self):
"""Returns the url to access a detail record for this user post."""
return reverse('userpost-detail', args=[str(self.id)])
class Comment(models.Model):
author = models.ForeignKey(User, default=1,
on_delete = models.CASCADE
)
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
def comment_default():
return {UserPost.id}
post = models.ForeignKey(UserPost, default= comment_default, on_delete=models.CASCADE, related_name="comments")
def __str__(self):
"""String for representing the comment object."""
return '%s - %s - %s' % (self.post.title, self.author, self.created_on)
def get_absolute_url(self):
return reverse('userpost-detail', args=[str(self.post.id)])
And my views:
class UserPostDetailView(generic.DetailView):
model = UserPost
#post = UserPost.objects.get(id=id)
#comments = Comment.objects.filter(Comment.post)
def get_context_data(self, **kwargs):
context = super(UserPostDetailView, self).get_context_data(**kwargs)
context['Comment'] = UserPost.comments
return context
class PostCreate(CreateView):
model = UserPost
fields = ['title', 'category', 'content']
class CommentCreate(CreateView):
model = Comment
fields = ['post','content']
And my html:
{% extends "base.html" %}
{% block page_content %}
<h1>Title: {{ userpost.title }}</h1>
<p><strong>Author:</strong> {{ userpost.author }}</p>
<p><strong>Content:</strong> {{ userpost.content }}</p>
<p><strong>Category:</strong> {{ userpost.category }}</p>
<a class="btn btn-primary" href="{% url 'comment-create' %}" role="button">Leave a Comment</a>
<h3>Comments:</h3>
{% for comment in userpost.comments.all %}
<p>
On {{comment.created_on.date }}
<b>{{ comment.author }}</b> wrote:
</p>
<p>{{ comment.content }}</p>
<hr>
{% endfor %}
{% endblock %}
You need to pass Post ID in your url for this.
path("comment/<int:post_id>/", CommentCreateView, name="comment-create")
Now in template
<a class="btn btn-primary" href="{% url 'comment-create' userpost.id %}" role="button">Leave a Comment</a>
Views
class CommentCreateView(CreateView):
model = Comment
fields = ['content'] # remove field post here
def form_valid(self, form):
form.instance.post_id = self.kwargs.get("post_id")
return super().form_valid(form)
Why my custom template tag doesn't work?
templatetags.py:
from django import template
from ..models import User
register = template.Library()
#register.inclusion_tag('main/post_detail.html', takes_context=True)
def get_user_liked_posts():
request = context['request']
user = User.objects.get(username=request.user.username)
liked_posts = []
for post in user.liked_posts.all():
liked_posts.append(post.name)
return {'liked_posts': liked_posts}
post_detail.html:
{% load static %}
{% load templatetags %}
<nav class="blog-pagination" aria-label="Pagination">
<span id="likes_count">{{ post.likes_count }}</span>
{% if post.name in liked_posts %}
<button id="like_button" class="btn btn-outline-primary btn-primary text-
white">Like</button>
{% else %}
<button id="like_button" class="btn btn-outline-primary">Like</button>
{% endif %}
</nav>
views.py:
class PostDetailView(DetailView):
model = Post
slug_field = 'url'
class LikePostView(View):
def post(self, request, slug):
post = Post.objects.get(id=request.POST['id'])
user = User.objects.get(username=request.user.username)
if request.POST['like'] == 'true':
post.likes_count += 1
user.liked_posts.add(post)
else:
post.likes_count -= 1
user.liked_posts.remove(post)
user.save()
post.save()
return redirect('post_detail', slug)
models.py:
class Post(models.Model):
"""
This is post model
"""
name = models.CharField(max_length=150, blank=False)
article = models.TextField(blank=False)
image = models.ImageField(upload_to='uploads/', blank=True)
likes_count = models.IntegerField(default=0)
url = models.CharField(max_length=150, blank=False)
def get_absolute_url(self):
return reverse('post_detail', kwargs={'slug': self.url})
I want to check if the post is in the liked post of the current user, but it doesn't work.
It doesn't show any errors, it just does nothing.
User in my app must like or unlike posts. In models, I have many to many relationship user with the post. I want to check if the user likes this post
The problem is that you don't even use the template tag, furthermore this is not even needed as you can simply write something like so in the template:
{% if post in request.user.liked_posts.all %}
A Liked post
{% else %}
Not a liked post
{% endif %}
But this is a bit inefficient as we are getting all the posts liked by the user just to check if they like some post. Also if this were in a loop with multiple posts we would be making a query for each post.
Instead we can simply annotate whether the user likes a post in the view itself using an Exists subquery [Django docs] on the through model of the many to many:
from django.db.models import Exists, OuterRef
class PostDetailView(DetailView):
model = Post
slug_field = 'url'
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(
liked_by_user=Exists(
User.liked_posts.through.objects.filter(
post_id=OuterRef("pk"),
user_id=self.request.user.id
)
)
)
return queryset
Now in the template we can simply write:
{% if post.liked_by_user %}
A Liked post
{% else %}
Not a liked post
{% endif %}
Note: Your way of saving the count similarly can simply be turned into an annotation using the Count aggregation function [Django
docs].
Generally one should not store calculated attributes in a column since
that might lead to inconsistent data when updating and forgetting to update the related count, etc.
I want to allow an user to be able to like an post only once. Here is my code so far:
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
likes = models.PositiveIntegerField(default=0)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})
In views.py:
def LikePostView(request, pk):
post = get_object_or_404(Post, pk=pk)
post.likes = post.likes+1
post.save()
return render(request, 'blog/like_done.html')
Code to like a post on frontend:
<p class="content">{{ post.likes }} likes</p>
{% if user.is_authenticated %}
</i>
{% else %}
<a onclick="alert('In order to like a post, you must be logged in!')" href='{% url "blog-home" %}'><i class="fa fa-thumbs-up" style="font-size:36px"></i></a>
{% endif %}
And like_done.html:
{% extends "blog/base.html" %}
{% block content %}
<h2 class="alert alert-success">Post Liked Successfuly!</h2>
<br>
<a class="btn btn-success" href="{% url 'blog-home' %}">Go Back Home?</a>
<br>
{% endblock content %}
So with all this, how would I make the user be allowed to only like a post once. I am only beginner, so please explain a little.
If I get your question right, You want a user to be able to like a post once, and when the user clicks on the like button the second time it should 'unlike' right?
If so, this is what you should do:
In you Post model, the like should be like this likes = models.ManyToManyField(User, related_name='user_likes'), a ManyToManyField.
Then in your views:
def LikePostView(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.user in post.likes.all():
post.likes.remove(request.user)
else:
post.likes.add(request.user)
return render(request, 'blog/like_done.html')
This way if the user clicks the like button he will be added to the likes Field and if he clicks it again he will be removed seems he is already there. So the user won't be able to like a post more than once.
I am creating a form using ModelForm to let the users upload a file along with a description . The is_valid() function isn't returning true and I am really confused. I have searched and there are many questions with same title as mine but they don't solve my problem.
here is forms.py:
class PostForm(forms.ModelForm):
document = forms.FileField(widget=forms.FileInput)
class Meta:
model = FeedModel
fields = ['description', 'document']
Here is models.py:
class FeedModel(models.Model):
description = models.CharField(max_length=255, blank=True)
document = models.FileField()
like = models.IntegerField(default=0)
dateTime = models.DateTimeField(auto_now=True, auto_created=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=0)
def get_absolute_url(self):
u=self.user
return reverse('home:feed',u.primary_key)
Here is views.py:
class PostView(CreateView):
form_class = PostForm
template_name = 'home/feedModel_form.html'
def get(self, request, *args, **kwargs):
form=self.form_class(None)
return render(request, self.template_name, {'form':form })
def post(self, request, *args, **kwargs):
logger = logging.getLogger(__name__)
form=self.form_class(request.POST)
if form.is_valid():
user=request.user
self.object=form.save(commit=False)
self.object.user=user
self.object.save()
logger.error("voila")
redirect({'home:feed'}, user.id)
return render(request, self.template_name, {'form':form })
def feedview(request, user_id):
user = User.objects.get(pk=user_id)
return render(request, 'home/feed.html', {'user': user})
Here is feedModel_form.html:
{% extends 'home/navbar.html' %}
{% block body %}
<div class="form">
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% include 'home/form.html' %}
<button type="submit" class="button button-block" `
name="reg_btn">Post</button>`
</form>
</div>
{% endblock %}
Here is form.html:
{% for field in form %}
<div class="field-wrap">
<label>
{{ field.label_tag }}<span class="req">*</span>
</label>
<div>{{ field }}</div>
</div>
{% endfor %}
To see why the form isn't valid, you should check form.errors.
One error will be because you have not passed request.FILES to the form.
form=self.form_class(request.POST, request.FILES)
There may be other errors as well. If you used {{ form }} in your template, Django would include the errors automatically. Since you are rendering the fields manually, it's up to you to include the errors.
The key problem here is that you have overridden post. That means that you're missing out on lots of the code from CreateView.
In your case, it looks like you could remove the post method, and simply override form_valid instead.
def form_valid(self, form):
self.object=form.save(commit=False)
self.object.user=user
self.object.save()
# Note that you had {'home:feed'} here which was incorrect
return redirect('home:feed', user_id)
Your document field expects an uploaded file and is required. In order for the form to actually get the file, you have to also pass it the uploaded file in views.py:
form = self.form_class(data=request.POST, files=request.FILES)
When a post is created, the user's User ID is saved to the post table under the user_id field. On the home page (for right now) I'm outputting all the posts in the post table. I want to get the username into the template based off of the user ID on the Post object.
models.py
class Post(models.Model):
user_id = models.IntegerField()
post_name = models.CharField(max_length=300)
total_votes = models.IntegerField(default=0)
created_date = models.DateTimeField()
views.py
#login_required
def home(request):
posts = Post.objects.raw('SELECT * FROM posts ORDER BY created_date DESC')
return render(request, 'home.html', {'posts': posts})
home.html
{% for post in posts %}
<div>
{{ post.post_name }}
<p>{{ post.created_date }}</p>
<p>{{ post.total_votes }}</p>
<p>{{ post.user_id }}</p>
</div>
{% endfor %}
Edit: Ended up changing query to:
SELECT
*, u.username
FROM posts p
LEFT JOIN auth_user u ON (p.user_id = u.id)
I didn't think you could add to the Post object and even call it as post.username. Regardless, if there's a better way to do it, let me know. Thanks!
Your whole logic is wrong :(
The user should be stored as a ForeignKey into the Post model. That is, a user may have multiple Posts but one Post may belong to one user. Something like that:
class Post(models.Model):
user = models.ForeignKey(User, related_name='posts')
post_name = models.CharField(max_length=300)
total_votes = models.IntegerField(default=0)
created_date = models.DateTimeField()
Note that the User model may be the built-in Django User model (from django.contrib.auth.models import User) or you own if you have customized it.
Database queries in Django, are not made with raw SQL. That's why the ORM exists. So, the view could be changed to:
#login_required
def home(request):
posts = Post.objects.order_by('-created_date')
return render(request, 'home.html', {'posts': posts})
Now you have a very verbose and pythonic access to the User model instance (and to the Post one) from your template.
{% for post in posts %}
<div>
{{ post.post_name }}
<p>{{ post.created_date }}</p>
<p>{{ post.total_votes }}</p>
<p>{{ post.user.username }}</p>
</div>
{% endfor %}
EXTRA TIP: Instead of href="posts/{{post.id}}", try to use the {% url %} template tag in order not to hardcode url paths like that.
It would be better to use ForeignKey for such purpose. For example:
class Post(models.Model):
user = models.ForeignKey(User)
post_name = models.CharField(max_length=300)
total_votes = models.IntegerField(default=0)
created_date = models.DateTimeField()
Make sure to import User in models.py.
Create a Form for creating Post:
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('post_name',)
Now, in views:
def create_post(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.user = request.user
instance.save()
return # Write return here
else:
form = PostForm()
context = {
'form': form
}
return render(request, 'template_name', context)
Create a template for this and use it to create posts.
In your view home:
#login_required
def home(request):
posts = Post.objects.order_by('-created_date')
return render(request, 'home.html', {'posts': posts})
In your template:
{% for post in posts %}
<div>
{{ post.post_name }}
<p>{{ post.created_date }}</p>
<p>{{ post.total_votes }}</p>
<p>{{ post.user.username }}</p>
</div>
{% endfor %}
It will work.