How to add comments on Python Django posts with logged in username? - python

Please let me know that where i am making mistake?
views.py
class AddComment(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
template_name = 'comment.html'
success_url = reverse_lazy('home')
def form_valid(self, form):
form.instance.name = self.request.user
form.instance.post_id = self.kwargs\['pk'\]
return super().form_valid(form)
Are these forms written correctly?
forms.py
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('body', )
widgets = {
# 'name': forms.TextInput(attrs={'class': 'form-control'}),
'body': forms.Textarea(attrs={'class': 'form-control'}),
}
Should I make any changes in models?
models.py
class Comment(models.Model):
post = models.ForeignKey(Post,
related_name='comments',
on_delete=models.CASCADE)
name = models.ForeignKey(
User,
on_delete=models.CASCADE,
)
body = models.TextField(max_length=240)
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
return '%s - %s' % (self.post.title, self.name)
This is comment section for letting user to comment on post with showing its own name?
comments.html
{% if not object.comments.all %}
<p>No comments yet...</p>
Add one
{% else %}
Add Comment
<br><br>
{% for comment in object.comments.all %}
<strong>{{ comment.name }} </strong> - <small>{{ comment.date_added }}</small>
<br>
{{ comment.body }}
<br><br>
<hr>
{% endfor %}
{% endif %}
Here is the urls of AddComment class view.
urls.py
path('post/<int:pk>/comment/', AddComment.as_view(), name='comment'),][1]

You did not state clearly what has gone wrong with your code. I would like to give some suggestions. First of all,
{% for comment in object.comments.all %}
...
{% endfor %}
You are putting this block of code inside {% if not object.comments.all %}...{% endif %} so it will not show on template if the comment section is not empty.
Also, this link:
Add Comment
should open a Django template form, where user can actually fill in the comment. After that, on POST request of the form, it will send comment data to the URL you put in action param of the form, as below:
<form action="{% url 'comment' post.pk %}" method="post">
[Comment code here]
</form>
which will link to this URL you provided:
path('post/<int:pk>/comment/', AddComment.as_view(), name='comment'),]
It will be better if you can provide your code in views.py as well to make it easier to track down where it goes wrong.

Related

Django: post form and post list on the same page

I make a site with multiple users, making posts with images and ability to add/remove friends.
So it's easy to make two different pages for post list and creating a new one. But of course it looks better when you can read posts and make new at the same place.
As I understand (learn django for less than a month), I can't connect 2 views to the same url, so the most logical way I see is to join 2 views in one, I also tried to play with template inheriting to render post form by including template, but actually it doesn't work.
Below you can see my views, Post model, and templates. Thank you for attention.
views.py:
from braces.views import SelectRelatedMixin
from . import models
from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin
class PostList(SelectRelatedMixin, generic.ListView):
model = models.Post
select_related = ('user',)
class CreatePost(LoginRequiredMixin, SelectRelatedMixin, generic.CreateView):
fields = ('post_message', 'post_image')
model = models.Post
select_related = ('user',)
def form_valid(self, form):
self.object = form.save(commit = False)
self.object.user = self.request.user
self.object.save()
return super().form_valid(form)
models.py:
import misaka
class Post(models.Model):
user = models.ForeignKey(User, on_delete = models.CASCADE, related_name = 'posts')
posted_at = models.DateTimeField(auto_now = True)
post_message = models.TextField()
message_html = models.TextField(editable = False)
post_image = models.ImageField(upload_to = 'postpics', blank = True)
def __str__(self):
return self.post_message
def save(self, *args, **kwargs):
self.message_html = misaka.html(self.post_message)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('posts:all')
class Meta:
ordering = ['-posted_at']
unique_together = ['user', 'post_message']
urls.py:
app_name = 'posts'
urlpatterns = [
path('', views.PostList.as_view(), name = 'all'),
path('new/', views.CreatePost.as_view(), name = 'create'),
]
post_form.html (template, that allows to make a new post, which will be seen in post_list.html):
{% extends 'posts/post_base.html'%}
{% block post_content %}
<div class="post-form">
<form action="{% url 'posts:create' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<p>{{ form.post_message }}</p>
<p>{{ form.post_image }}</p>
<input id='post-submit' type="submit" value="Post">
</form>
</div>
{% endblock %}
post_list.html:
{% extends 'posts/post_base.html'%}
{% block post_content %}
<div class="post-container">
{% for post in post_list %}
<div class="current-post-container">
{% include 'posts/_post.html'%}
</div>
{% endfor %}
</div>
{% endblock %}
_post.html(pages, which render by Misaka):
<div class="post-info">
<h5 id='post-owner' >{{post.user.first_name}} {{post.user.last_name}}</h5>
<h6>{{ post.posted_at }}</h6>
<p>{{ post.message_html|safe }}</p>
<div>
<img class='post-image' src="/media/{{ post.post_image }}" alt="">
<div>
{% if user.is_authenticated and post.user == user and not hide_delete %}
<a href="{% url 'posts:delete' pk=post.pk %}" title = 'delete'>Delete</a>
{% endif %}
</div>
</div>
</div>
post_base.html:
{% extends 'base.html' %}
{% block content%}
{% block prepost %}{% endblock %}
{% block post_content %}{% endblock %}
{% block post_post %}{% endblock %}
{% endblock %}
EDIT:
Task was solved. I added two template_name strings to both of my views, so now they look like:
CreatePost in views.py:
class CreatePost(LoginRequiredMixin, SelectRelatedMixin, generic.CreateView):
fields = ('post_message', 'post_image')
model = models.Post
select_related = ('user',)
template_name = 'posts/post_list.html'
template_name = 'posts/post_form.html'
def form_valid(self, form):
self.object = form.save(commit = False)
self.object.user = self.request.user
self.object.save()
return super().form_valid(form)
PostList in views.py:
class PostList(SelectRelatedMixin, generic.ListView):
model = models.Post
select_related = ('user',)
template_name = 'posts/post_list.html'
template_name = 'posts/post_form.html'
You can put the post_create_form on the same page as post_list_view there is no need to make a separate view for post creation but You need to make ones for editing and deleting.
You can give all of these views the same HTML page with different URLs.
Using template_name = 'example/example.html' ,in Class_Based_Views.
I hope I understand your problem if not clarify more why you can't join two views in one.
def posts(request):
posts = Post.objects.all()
form = PostForm(request.POST or None, request.FILES or None)
if request.method == "POST":
if form.is_valid():
...
context={
'posts' : page_obj,
'create_or_update_post_form' : form,
}
return render(request, 'post/posts.html',context)
Do you struggle to do this in Class-based-view?
You can do easily with django class based views.
Create views as
from django.views.generic import ListView
from django.views.generic.edit import CreateView
class ModelCreate(CreateView):
model = ModelName
fields = ['field1', 'field2']
template_name = 'same_page.html'
success_url = reverse_lazy('list_view')
class ModelList(CreateView, ListView):
model = ModelName
fields = ['field1', 'field2']
paginate_by = 5
template_name = 'same_page.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['form'] = self.get_form()
return context
# If form post redirect to ModelCreate View
def post(self, request, *args, **kwargs):
return ModelCreate.as_view()(request)
app/urls.py
from django.urls import path
from app import views
path('list', views.ModelList.as_view(), name='list_view'),
path('add', views.ModelCreate.as_view(), name='add_view'),
Finally in templates/same_page.html
<div class="row">
<div class="col-sm-5">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{form.as_p}}
<button type="submit" value="submit" class="btn btn-primary btn-sm float-right">Submit</button>
</form>
</div>
<div class="col-sm-5">
{% if object_list %}
{% for object in object_list %}
<p>{{object.field1}}</p>
<p>{{object.field2}}</p>
{% endfor %}
{% endif %}
</div>
</div>
Hope, this helps.

Comments under Post

I created model Post and Comment, now I want to display comments from Post but I have a problem. In tutorial there is a line of code in html {% for comment in post.comments.all %} but isn't working in my app. If I set {% for comment in comments %} it works but displays all comments from models (I want only comments from the post). How to fix that? Below I pasted my code.
models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=64, blank=False, null=False)
short_text = models.TextField(blank=False, null=False)
text = models.TextField(blank=False, null=False)
image = models.TextField(blank=False, null=False)
date = models.DateTimeField(auto_now_add=True, blank=True)
views = models.IntegerField(default=0)
class Comment(models.Model):
post = models.ForeignKey('main.Post', on_delete=models.CASCADE)
author = models.CharField(max_length=200)
text = models.TextField()
created_date = models.DateTimeField(auto_now_add=True, blank=True)
approved_comment = models.BooleanField(default=False)
def approve(self):
self.approved_comment = True
self.save()
def __str__(self):
return self.text
views.py
from django.shortcuts import get_object_or_404, render, redirect
from .models import Post, Comment
def index_pl(request):
posts = Post.objects.all()
return render(request, 'index_pl.html', {'posts': posts})
def single_article_pl(request, id):
posts = get_object_or_404(Post, pk=id)
posts.views = posts.views + 1
posts.save()
comments = Comment.objects.all()
return render(request, 'single_article_pl.html', {'posts': posts, 'comments': comments})
html
{% for comment in post.comments.all %}
<div class="comment">
<div class="date">{{ comment.created_date }}</div>
<strong>{{ comment.author }}</strong>
<p>{{ comment.text|linebreaks }}</p>
</div>
{% empty %}
<p>No comments here yet :(</p>
{% endfor %}
admin.py
from django.contrib import admin
from .models import Post, Comment
admin.site.register(Post)
admin.site.register(Comment)
In tutorial there is a line of code in html {% for comment in post.comments.all %} but isn't working in my app.
This is likely because they specified the related_name=… parameter [Django-doc] in the ForeignKey from Comment to Post, like:
# Option 1: set the related_name to 'comments'
class Comment(models.Model):
post = models.ForeignKey(
'main.Post',
related_name='comments',
on_delete=models.CASCADE
)
# …
The related_name=… specifies the name of the relation in reverse, so in this case accessing the Comments of a given Post object. By default this is model_name_set, so in this case comment_set.
You thus can either specify a related name; or you can acces the comment_set manager:
Option 2: use the default related_name
{% for comment in post.comment_set.all %}
<div class="comment">
<div class="date">{{ comment.created_date }}</div>
<strong>{{ comment.author }}</strong>
<p>{{ comment.text|linebreaks }}</p>
</div>
{% empty %}
<p>No comments here yet :(</p>
{% endfor %}

Posts not showing author who wrote it in Django

Basically, I'm writing an app in which people can make blog and image posts. So far, I've completed users to be able to write text posts. However, when I try to create a post, it returns "By: None" when it should be returning "By: shrey". In this case, Bob is the author. Here's an image:
Here's an image for the post creation view:
Theoretically, when I enter a post it should say who it was written by.
Here's the template for the create post:
{% extends "social/base.html" %}
{% load crispy_forms_tags %}
{% block content4 %}
<h1>Make Your Post</h1>
<p>Write a post / Share an image</p>
<br>
<div class="container">
<form method="post">
{% csrf_token %}
{{form|crispy}}
<button type="submit" name="button">Make Post</button>
</form>
</div>
{% endblock content4 %}
Here's the function for the create post view:
class PostCreateView(CreateView):
model = Posts
fields = ['post_title', 'post_text_content']
def form_valid(self, form):
form.instance.author = self.request.user
print(self.request.user)
return super().form_valid(form)
Thank you in advance.
EDIT: Home Page Template (template which displays the posts):
{% extends "social/base.html" %}
{% block content %}
<h1>Your Feed</h1>
<p>This is your feed. Here, you'll see posts from people you follow.</p>
{% if user.is_authenticated %}
<p>You are logged in as {{user.username}}. This is your feed.</p>
{% else %}
<p>You are not logged in. This is a random feed.</p>
{% endif %}
{% for post in posts %}
<h1>{{ post.post_title }}</h1>
<p>By {{ post.post_author }} on <i>{{ post.post_date }}</i></p>
<p>{{ post.post_text_content }}</p>
{% endfor %}
Click here to make a post.
<br>
Click here to logout.
<br>
Click here to login.
<br>
Click here to sign up and make an account.
<!--<p>Want to post something? Enter your info here: </p> -->
{% endblock content %}
Posts Model:
class Posts(models.Model):
post_title = models.CharField(max_length = 40, help_text = 'Enter post title')
post_text_content = models.TextField(max_length = 1000)
post_author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
post_date = models.DateField(auto_now = True, auto_now_add = False)
#Make optional Image Field
class Meta:
ordering = ['post_title', 'post_author', 'post_date', 'post_text_content']
def __str__(self):
return self.post_title
def get_absolute_url(self):
return reverse('social-home')
The name of the field is post_author, not author, hence you should set post_author:
class PostCreateView(CreateView):
model = Posts
fields = ['post_title', 'post_text_content']
def form_valid(self, form):
form.instance.post_author = self.request.user
return super().form_valid(form)
That being said, normally in Django one does not prefixes the model fields with the name of the model. One reason not to do that is that you can define abstract models where you define the field once, and then use inheritance to add the field to other models.

NoReverseMatch at /post/1/log/ Reverse for 'log-create' with keyword arguments '{'post_id': ''}' not found

I have a Post model with a whole bunch of posts. I also have a log model which has a foreign key field to the Post model. Essentially the Log model stores log entries for the Posts in the Post model (basically Post comments). Everything was going great. I have been using CBV for my post models and I used a CBV to List my log entries. I then added a link to redirect me to the Log CreateView using the following anchor tag:
<a class="btn" href="{% url 'log-create' post_id=logs.post_id %}">Add Entry</a>
When the NoReverse errors started occuring. When I change the log.post_id to 1, the page loads correctly. This leads me to believe that the log.post_id is not returning any value. Another thought that I had was that because this anchor tag was on the LogListView there were multiple log entries so it didn't know which post_id to use. But I used the get_queryset function on this view to make sure that only logs related to a single post are returned. In my mind the log.post_id should work.
My models are:
class Post(models.Model):
title = models.CharField(max_length=100, blank=True)
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
overview = models.TextField(blank=True)
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.id})
def __str__(self):
return self.title
class Log(models.Model):
post = models.ForeignKey(Post, default=None, on_delete=models.CASCADE)
log_entry = models.TextField(max_length=500, blank=True)
log_author = models.ForeignKey(User, on_delete=models.CASCADE)
date_posted = models.DateTimeField(default=timezone.now)
My Views:
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView, CreateView
from .models import Post, Log
from django.http import HttpResponseRedirect
from django.contrib.auth.mixins import LoginRequiredMixin
class LogListView(ListView):
model = Log
template_name = 'blog/log_entries.html'
context_object_name = 'logs'
ordering = ['-date_posted']
def get_queryset(self):
self.post = get_object_or_404(Post, log=self.kwargs['pk'])
return Log.objects.filter(post=self.post)
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super(LogListView, self).get_context_data(**kwargs)
# Add in a QuerySet of all images related to post
context['post'] = Post.objects.all()
return context
class LogCreateView(LoginRequiredMixin, CreateView):
model = Log
fields = [
'log_entry'
]
def form_valid(self, form):
form.instance.log_author = self.request.user
post = Post.objects.get(pk=self.kwargs['post_id'])
return super().form_valid(form)
My urls.py
from django.urls import path, include
from . import views
from .views import LogListView, LogCreateView
urlpatterns = [
path('', PostListView.as_view(), name='blog-home'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/new/', PostCreateView.as_view(), name='post-create'),
path('post/<int:pk>/log/', LogListView.as_view(), name='log-list'),
path('post/<int:post_id>/log/new/', LogCreateView.as_view(), name='log-create'),
]
And Lastly, my template:
{% extends "blog/base.html"%}
{% block body_class %} home-section {% endblock %}
{% block content %}
<div class="container">
<h2>Log Entries</h2>
{% for log in logs %}
<div class="row">
<article class="content-section">
<div class="article-metadata log-metadata">
<a class="mr-2" href="{% url 'profile' user=log.log_author %}">{{ log.log_author }}</a>
<small class="text-muted">{{ log.date_posted|date:"d F Y" }}</small>
{% if request.user.is_authenticated and request.user == log.log_author %}
<ion-icon name="trash"></ion-icon>
{% endif %}
</div>
<p class="">{{ log.log_entry }}</p>
</article>
</div>
{% endfor %}
<a class="btn" href="{% url 'log-create' post_id=logs.post_id %}">Add Entry</a>
</div>
{% endblock content %}
I think I am correctly passing a parameter to the url. this is evident from when I make post_id=1. But I am not sure I am calling it correctly. Any help on this issue would be great thanks.
UPDATED: I edited my context_object_name in my LogListView to logs to make the for loop less confusing. Essentially I am trying to get one anchor tag at the bottom of all the log entries to redirect to the Add entry page.
I suggest an approach that renders a link only if there are objects available, using the first element:
</article>
</div>
{% if forloop.first %}<a class="btn" href="{% url 'log-create' post_id=log.post.id %}">Add Entry</a>{% endif %}
{% endfor %}
This log.post.id means get id of post object (foreign key) of log object.

How to display post and related comments on single page?

I am unable to design a code to render one particular post and it's related comments. The issue is maybe in views.py or the url.
I have looked at multiple sources without any results. I am a novice to coding and feel like I am missing some essential point. Posts and comments are getting created correctly and all comments get the correct post_id assigned.
My models.py is set up like this:
class Post(models.Model):
title = models.CharField(max_length=1000)
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('blog-home')
class Comment(models.Model):
cid = models.AutoField(primary_key=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
comment = models.TextField()
comment_date = models.DateTimeField(default=timezone.now)
def save(self, *args, **kwargs):
super(Comment, self).save(*args, **kwargs)
def __str__(self):
return self.comment
def get_absolute_url(self):
return reverse('blog-home')
My views.py is set up like this:
class PostDetailView(DetailView):
model = Post
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comment_list'] = Comment.objects.filter(post=WHAT SHOULD GO HERE?)
return context
I need to pass the Post.id or primary key of the post in the filter above. Can someone explain what it should be?
The url used to access the post detail is as follows:
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail')
I get the post detail view rendered out as the author, title and content of the post when I have the following in views.py:
class PostDetailView(DetailView):
model = Post
The template for that is as below:
{% extends "blog/base.html" %}
{% block content%}
<article class="media content-section">
<img class="rounded-circle article-img" src="{{object.author.profile.image.url}}">
<div class="media-body">
<div class="article-metadata">
<a class="mr-2" href="{% url 'user-posts' object.author.username %}">{{ object.author }}</a>
<small class="text-muted">{{ object.date_posted|date:"F d, Y P e" }}</small>
{% if object.author == user %}
<div><a class="btn btn-secondary btn-sm m-1 mb-1" href="{% url 'post-update' object.id%}">Update</a>
<a class="btn btn-danger btn-sm m-1 mb-1" href="{% url 'post-delete' object.id%}">Delete</a></div>
{% endif %}
</div>
<h2 class="article-title">{{ object.title }}</h2>
<p class="article-content">{{ object.content }}</p>
</div>
</article>
{% for comment in comment_list %}
<div class='article-content'>
<p>{{comment}}</p>
</div>
{% endfor %}
{% endblock %}
How should I take the post.id or pk of the Post and use it to filter the comments related only to that particular post?
Also, what is a good way to set up a template for rendering the queryset?
You should be able to iterate over the reverse link from Post object to the Comment objects linked to it (by default as comment_set) in your template:
{% for comment in post.comment_set %}
If you want greater control you inject a queryset into the context, something like this, to get the most recent six comments only.
"comments": post.comment_set.order_by("-comment_date")[:6]
The post object selected should be available as self.object in a DetailView and will be default be injected into the render context as object. An invaluable resource for navigating the structure of Django Class-based views is the Classy CBV
site.
Warning: this is "off the top of my head" so don't assume it's all perfect.
A single object will have access to its related objects.
Try this:
class PostDetailView(DetailView):
model = Post
# There's no need to define get_context_data() here
Now your template will have the post available as post (and also object).
All of the comments that have this post are available on the template like this:
{% for comment in post.comment_set.all %}
<div class='article-content'>
<p>{{ comment }}</p>
</div>
{% endfor %}

Categories