Remove comments
This commit is contained in:
parent
db31dcadeb
commit
78603c5afc
12 changed files with 36 additions and 319 deletions
|
@ -1,9 +1,11 @@
|
|||
import copy
|
||||
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin import register
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from .models import Article, Comment, Page, User
|
||||
from .models import Article, Page, User
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
|
@ -29,7 +31,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||
{
|
||||
"fields": [
|
||||
("title", "slug"),
|
||||
("author", "comments_allowed"),
|
||||
("author",),
|
||||
("status", "published_at"),
|
||||
("created_at", "updated_at"),
|
||||
"views_count",
|
||||
|
@ -100,27 +102,3 @@ class PageAdmin(ArticleAdmin):
|
|||
article_fieldsets = ArticleAdmin.fieldsets
|
||||
article_fieldsets[0][1]["fields"][0] = ("title", "slug", "position")
|
||||
return article_fieldsets
|
||||
|
||||
|
||||
@register(Comment)
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"username",
|
||||
"email",
|
||||
"content",
|
||||
"article",
|
||||
"created_at",
|
||||
"status",
|
||||
"user_notified",
|
||||
)
|
||||
list_filter = ("status",)
|
||||
search_fields = ("username", "email", "content")
|
||||
actions = ["approve_comments", "reject_comments"]
|
||||
|
||||
def approve_comments(self, request, queryset):
|
||||
count = queryset.update(status=Comment.APPROVED, user_notified=False)
|
||||
messages.success(request, f"Approved {count} message(s).")
|
||||
|
||||
def reject_comments(self, request, queryset):
|
||||
count = queryset.update(status=Comment.REJECTED, user_notified=False)
|
||||
messages.success(request, f"Rejected {count} message(s).")
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
from articles.models import Comment
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
required_css_class = "required"
|
||||
error_css_class = "error"
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ["username", "email", "content"]
|
||||
|
||||
def as_table(self):
|
||||
"Return this form rendered as HTML <tr>s -- excluding the <table></table>."
|
||||
return self._html_output(
|
||||
normal_row="<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>",
|
||||
error_row=(
|
||||
'<tr class="error nonfield"><td colspan="2">%s</td></tr>'
|
||||
'<tr class="spacer"><td colspan="2"></td></tr>'
|
||||
),
|
||||
row_ender="</td></tr>",
|
||||
help_text_html='<br><span class="helptext">%s</span>',
|
||||
errors_on_separate_row=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
defaults = {
|
||||
"label_suffix": "",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
super().__init__(*args, **defaults)
|
|
@ -1,28 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.core.mail import mail_admins
|
||||
from django.core.management import BaseCommand
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from articles.models import Comment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Check for pending comments and send an email to the admin."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
count = Comment.objects.filter(status=Comment.PENDING).count()
|
||||
if count:
|
||||
url = reverse("admin:articles_comment_changelist")
|
||||
url = (settings.BLOG["base_url"] + url).replace(
|
||||
"//", "/"
|
||||
) + "?status__exact=pending"
|
||||
message = (
|
||||
ngettext(
|
||||
"There is %(count)d comment pending review.\n%(url)s",
|
||||
"There are %(count)d comments pending review.\n%(url)s",
|
||||
count,
|
||||
)
|
||||
% {"count": count, "url": url}
|
||||
)
|
||||
mail_admins("Comments pending review", message)
|
|
@ -1,54 +0,0 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import mail_admins, send_mass_mail
|
||||
from django.core.management import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.template import Context, Engine
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from articles.models import Comment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Check for pending comments and send an email to the admin."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
to_notify = (
|
||||
Comment.objects.filter(
|
||||
Q(status=Comment.APPROVED) | Q(status=Comment.REJECTED),
|
||||
user_notified=False,
|
||||
)
|
||||
.exclude(email=None)
|
||||
.exclude(email="")
|
||||
)
|
||||
by_email = {}
|
||||
for comment in to_notify:
|
||||
if comment.email not in by_email:
|
||||
by_email[comment.email] = {"approved": [], "rejected": []}
|
||||
if comment.status == Comment.APPROVED:
|
||||
by_email[comment.email]["approved"].append(comment)
|
||||
elif comment.status == Comment.REJECTED:
|
||||
by_email[comment.email]["rejected"].append(comment)
|
||||
|
||||
email_data = []
|
||||
for email, comments in by_email.items():
|
||||
approved = comments["approved"]
|
||||
rejected = comments["rejected"]
|
||||
subject = ngettext(
|
||||
"Your comment has been moderated.",
|
||||
"Your comments have been moderated.",
|
||||
len(approved) + len(rejected),
|
||||
)
|
||||
blog_title = settings.BLOG["title"]
|
||||
message = render_to_string(
|
||||
"articles/comments_notification_email.txt",
|
||||
{"approved": approved, "rejected": rejected, "blog_title": blog_title},
|
||||
).replace("'", "'")
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
recipient_list = [email]
|
||||
email_data.append((subject, message, from_email, recipient_list))
|
||||
send_mass_mail(tuple(email_data))
|
||||
to_notify.update(user_notified=True)
|
20
articles/migrations/0021_auto_20201110_1623.py
Normal file
20
articles/migrations/0021_auto_20201110_1623.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.1.1 on 2020-11-10 15:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("articles", "0020_auto_20200903_2157"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="article",
|
||||
name="comments_allowed",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Comment",
|
||||
),
|
||||
]
|
|
@ -48,7 +48,6 @@ class Article(AdminUrlMixin, models.Model):
|
|||
author = models.ForeignKey(User, on_delete=models.PROTECT, default=1)
|
||||
views_count = models.IntegerField(default=0)
|
||||
slug = models.SlugField(unique=True, max_length=255)
|
||||
comments_allowed = models.BooleanField(default=True)
|
||||
|
||||
objects = models.Manager()
|
||||
without_pages = ArticleManager()
|
||||
|
@ -118,47 +117,3 @@ class Page(Article):
|
|||
|
||||
class Meta:
|
||||
ordering = ["position", "-published_at"]
|
||||
|
||||
|
||||
class Comment(AdminUrlMixin, models.Model):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
STATUS_CHOICES = (
|
||||
(PENDING, "Pending"),
|
||||
(APPROVED, "Approved"),
|
||||
(REJECTED, "Rejected"),
|
||||
)
|
||||
username = models.CharField(
|
||||
max_length=255, help_text="Will be displayed with your comment."
|
||||
)
|
||||
email = models.EmailField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=(
|
||||
"Not mandatory, fill only if you want me to be able to contact you. "
|
||||
"It will never be displayed here nor shared with any third party."
|
||||
),
|
||||
)
|
||||
content = models.TextField(
|
||||
max_length=500,
|
||||
help_text="Your comment, limited to 500 characters. No formatting.",
|
||||
)
|
||||
article = models.ForeignKey(
|
||||
Article, on_delete=models.CASCADE, related_name="comments"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=PENDING)
|
||||
user_notified = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ["created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.username} - {self.content[:50]}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.article.get_absolute_url() + "#" + str(self.id)
|
||||
|
||||
def get_full_absolute_url(self, request: HttpRequest = None):
|
||||
return self.article.get_full_absolute_url(request) + "#" + str(self.id)
|
||||
|
|
|
@ -211,33 +211,6 @@ textarea, input {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* COMMENTS */
|
||||
.comment {
|
||||
background-color: var(--background2);
|
||||
border-radius: .5ex;
|
||||
padding: .5em 1em;
|
||||
}
|
||||
|
||||
.comment + .comment, .comments form {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.comment p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment .metadata {
|
||||
color: var(--main3);
|
||||
}
|
||||
|
||||
.comment:target {
|
||||
background-color: var(--warning-background);
|
||||
}
|
||||
|
||||
.permalink {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* MESSAGES */
|
||||
.messages p {
|
||||
background-color: var(--background2);
|
||||
|
|
|
@ -5,13 +5,19 @@
|
|||
{% endblock %}
|
||||
{% block content %}
|
||||
<article class="article-detail">
|
||||
<h1>{{ article.title }}{% if article.status != article.PUBLISHED %} ({{ article.status }}){% endif %}</h1>
|
||||
<h1>{{ article.title }}{% if article.status != article.PUBLISHED %} (
|
||||
{{ article.status }}){% endif %}</h1>
|
||||
{% include "articles/metadata_snippet.html" %}
|
||||
<div>
|
||||
{{ article.get_formatted_content|safe }}
|
||||
</div>
|
||||
</article>
|
||||
{% if article.comments_allowed %}
|
||||
{% include 'articles/comment_snippet.html' %}
|
||||
{% endif %}
|
||||
<section class="comments">
|
||||
<h2>Comments</h2>
|
||||
<p>
|
||||
Comments are hard to manage. I tried enabling them but I only got spam.
|
||||
If you want to react to this article or interact with me, please head to the
|
||||
<a href="/about-me/">about me</a> page 😉.
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<section class="comments">
|
||||
<h2>Comments</h2>
|
||||
{% for comment in comments %}
|
||||
<article id="{{ comment.id }}" class="comment">
|
||||
<p class="metadata">
|
||||
<a class="permalink" title="Permalink" href="#{{ comment.id }}">🔗</a>
|
||||
<span class="username">{{ comment.username }}</span> |
|
||||
<time datetime="{{ comment.created_at|date:CUSTOM_ISO }}">
|
||||
{{ comment.created_at|date:"DATETIME_FORMAT" }}
|
||||
</time>
|
||||
{% include "articles/admin_link_snippet.html" with article=comment %}
|
||||
</p>
|
||||
<p class="content">
|
||||
{{ comment.content|linebreaksbr }}
|
||||
</p>
|
||||
</article>
|
||||
{% empty %}
|
||||
<p>
|
||||
No reaction yet, write your own!
|
||||
</p>
|
||||
{% endfor %}
|
||||
<form id="comment-form" action="{% url 'create-comment' slug=article.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<button type="submit">Submit</button>
|
||||
<p class="helptext">
|
||||
Your comment may not be approved if it's not respectful, on topic or spammy.
|
||||
If you feel I've made a mistake with your comment, please
|
||||
<a href="/about-me/">send me a message</a>!
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
|
@ -1,20 +0,0 @@
|
|||
Hello,
|
||||
|
||||
This is a quick (automated) notification about the comments you left
|
||||
on {{ blog_title }}.
|
||||
|
||||
{% if approved %}Approved:
|
||||
{% for comment in approved %}* {{ comment.get_full_absolute_url }}
|
||||
{% endfor %}{% endif %}
|
||||
|
||||
{% if rejected %}Rejected:
|
||||
{% for comment in rejected %}* #{{comment.id}} on {{ comment.article.get_full_absolute_url }}:
|
||||
{{ comment.content }}
|
||||
|
||||
{% endfor %}{% endif %}
|
||||
|
||||
You received this notification because you left your email address in the
|
||||
comment form.
|
||||
|
||||
Cheers,
|
||||
Gabriel
|
|
@ -1,14 +1,11 @@
|
|||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import F
|
||||
from django.views import generic
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from articles.forms import CommentForm
|
||||
from articles.models import Article, Comment, Page
|
||||
from articles.models import Article, Page
|
||||
|
||||
|
||||
class ArticlesListView(generic.ListView):
|
||||
|
@ -40,9 +37,8 @@ class DraftsListView(generic.ListView, LoginRequiredMixin):
|
|||
return context
|
||||
|
||||
|
||||
class ArticleDetailView(FormMixin, generic.DetailView):
|
||||
class ArticleDetailView(generic.DetailView):
|
||||
model = Article
|
||||
form_class = CommentForm
|
||||
context_object_name = "article"
|
||||
template_name = "articles/article_detail.html"
|
||||
|
||||
|
@ -52,14 +48,6 @@ class ArticleDetailView(FormMixin, generic.DetailView):
|
|||
return queryset
|
||||
return queryset.filter(status=Article.PUBLISHED)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
article = self.object
|
||||
if hasattr(article, "article"):
|
||||
article = article.article
|
||||
context["comments"] = article.comments.filter(status=Comment.APPROVED)
|
||||
return context
|
||||
|
||||
def get_object(self, queryset=None) -> Union[Article, Page]:
|
||||
obj = super().get_object(queryset) # type: Article
|
||||
if hasattr(obj, "page"):
|
||||
|
@ -69,34 +57,3 @@ class ArticleDetailView(FormMixin, generic.DetailView):
|
|||
obj.save(update_fields=["views_count"])
|
||||
|
||||
return obj
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object() # type: Union[Article, Page]
|
||||
form = self.get_form()
|
||||
|
||||
if not self.object.comments_allowed:
|
||||
messages.error(self.request, "Comments are disabled on this article.")
|
||||
# Bypassing self.form_invalid because we don't want its error message
|
||||
return super().form_invalid(form)
|
||||
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(
|
||||
self.request,
|
||||
'Your comment couldn\'t be saved, see <a href="#comment-form">the form below</a>.',
|
||||
)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
comment = form.save(commit=False)
|
||||
comment.article = self.object
|
||||
comment.save()
|
||||
messages.success(self.request, "Comment successfully saved, pending review.")
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.object.get_absolute_url()
|
||||
|
|
|
@ -34,9 +34,6 @@ urlpatterns = [
|
|||
path("feed/", feeds.CompleteFeed(), name="complete-feed"),
|
||||
path("<slug:slug>", html.ArticleDetailView.as_view(), name="article-detail-old"),
|
||||
path("<slug:slug>/", html.ArticleDetailView.as_view(), name="article-detail"),
|
||||
path(
|
||||
"<slug:slug>/comment/", html.ArticleDetailView.as_view(), name="create-comment"
|
||||
),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
Reference in a new issue