Remove comments

This commit is contained in:
Gabriel Augendre 2020-11-10 16:26:27 +01:00
parent db31dcadeb
commit 78603c5afc
No known key found for this signature in database
GPG key ID: 1E693F4CE4AEE7B4
12 changed files with 36 additions and 319 deletions

View file

@ -1,9 +1,11 @@
import copy
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin import register from django.contrib.admin import register
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.shortcuts import redirect from django.shortcuts import redirect
from .models import Article, Comment, Page, User from .models import Article, Page, User
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
@ -29,7 +31,7 @@ class ArticleAdmin(admin.ModelAdmin):
{ {
"fields": [ "fields": [
("title", "slug"), ("title", "slug"),
("author", "comments_allowed"), ("author",),
("status", "published_at"), ("status", "published_at"),
("created_at", "updated_at"), ("created_at", "updated_at"),
"views_count", "views_count",
@ -100,27 +102,3 @@ class PageAdmin(ArticleAdmin):
article_fieldsets = ArticleAdmin.fieldsets article_fieldsets = ArticleAdmin.fieldsets
article_fieldsets[0][1]["fields"][0] = ("title", "slug", "position") article_fieldsets[0][1]["fields"][0] = ("title", "slug", "position")
return article_fieldsets 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).")

View file

@ -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)

View file

@ -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)

View file

@ -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("&#x27;", "'")
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)

View 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",
),
]

View file

@ -48,7 +48,6 @@ class Article(AdminUrlMixin, models.Model):
author = models.ForeignKey(User, on_delete=models.PROTECT, default=1) author = models.ForeignKey(User, on_delete=models.PROTECT, default=1)
views_count = models.IntegerField(default=0) views_count = models.IntegerField(default=0)
slug = models.SlugField(unique=True, max_length=255) slug = models.SlugField(unique=True, max_length=255)
comments_allowed = models.BooleanField(default=True)
objects = models.Manager() objects = models.Manager()
without_pages = ArticleManager() without_pages = ArticleManager()
@ -118,47 +117,3 @@ class Page(Article):
class Meta: class Meta:
ordering = ["position", "-published_at"] 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)

View file

@ -211,33 +211,6 @@ textarea, input {
width: 100%; 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 */
.messages p { .messages p {
background-color: var(--background2); background-color: var(--background2);

View file

@ -5,13 +5,19 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<article class="article-detail"> <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" %} {% include "articles/metadata_snippet.html" %}
<div> <div>
{{ article.get_formatted_content|safe }} {{ article.get_formatted_content|safe }}
</div> </div>
</article> </article>
{% if article.comments_allowed %} <section class="comments">
{% include 'articles/comment_snippet.html' %} <h2>Comments</h2>
{% endif %} <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&nbsp;😉.
</p>
</section>
{% endblock %} {% endblock %}

View file

@ -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>

View file

@ -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

View file

@ -1,14 +1,11 @@
from typing import Union from typing import Union
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import F from django.db.models import F
from django.views import generic from django.views import generic
from django.views.generic.edit import FormMixin
from articles.forms import CommentForm from articles.models import Article, Page
from articles.models import Article, Comment, Page
class ArticlesListView(generic.ListView): class ArticlesListView(generic.ListView):
@ -40,9 +37,8 @@ class DraftsListView(generic.ListView, LoginRequiredMixin):
return context return context
class ArticleDetailView(FormMixin, generic.DetailView): class ArticleDetailView(generic.DetailView):
model = Article model = Article
form_class = CommentForm
context_object_name = "article" context_object_name = "article"
template_name = "articles/article_detail.html" template_name = "articles/article_detail.html"
@ -52,14 +48,6 @@ class ArticleDetailView(FormMixin, generic.DetailView):
return queryset return queryset
return queryset.filter(status=Article.PUBLISHED) 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]: def get_object(self, queryset=None) -> Union[Article, Page]:
obj = super().get_object(queryset) # type: Article obj = super().get_object(queryset) # type: Article
if hasattr(obj, "page"): if hasattr(obj, "page"):
@ -69,34 +57,3 @@ class ArticleDetailView(FormMixin, generic.DetailView):
obj.save(update_fields=["views_count"]) obj.save(update_fields=["views_count"])
return obj 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()

View file

@ -34,9 +34,6 @@ urlpatterns = [
path("feed/", feeds.CompleteFeed(), name="complete-feed"), 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-old"),
path("<slug:slug>/", html.ArticleDetailView.as_view(), name="article-detail"), path("<slug:slug>/", html.ArticleDetailView.as_view(), name="article-detail"),
path(
"<slug:slug>/comment/", html.ArticleDetailView.as_view(), name="create-comment"
),
] ]
if settings.DEBUG: if settings.DEBUG: