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 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).")
|
|
||||||
|
|
|
@ -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)
|
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)
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 😉.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% 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 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()
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Reference in a new issue