diff --git a/README.rst b/README.rst
index 5c15719..4b03c0a 100644
--- a/README.rst
+++ b/README.rst
@@ -53,6 +53,12 @@
django-comments-dab
===================
+Thanks https://www.pythonanywhere.com/
+
+**Here is a** live_ **demo**
+
+.. _live: https://rmustafa.pythonanywhere.com/
+
Full Documentation_
.. _Documentation: https://django-comment-dab.readthedocs.io/
@@ -99,6 +105,8 @@ It allows you to integrate commenting functionality with any model you have e.g.
8. Resolve or reject flag. This is used to revoke the flagged comment state (admins and moderators)
+ 9. Follow and unfollow thread. (authenticated users)
+
- All actions are done by Fetch API since V2.0.0
- Bootstrap 4.1.1 is used in comment templates for responsive design.
@@ -281,6 +289,8 @@ The icons are picked from Feather_. Many thanks to them for the good work.
.. _Feather: https://feathericons.com
+Email's HTML template is used from https://github.com/leemunroe/responsive-html-email-template
+
Contributing
============
diff --git a/comment/__init__.py b/comment/__init__.py
index 1d47a75..d5950e9 100644
--- a/comment/__init__.py
+++ b/comment/__init__.py
@@ -4,24 +4,20 @@
def _get_version():
- _parent_project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- with open(os.path.join(_parent_project_dir, 'VERSION')) as version_file:
- version = version_file.read().strip()
-
- if version.lower().startswith('v'):
- version = version[1:]
+ parent_project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ with open(os.path.join(parent_project_dir, 'VERSION')) as version_file:
+ version = version_file.read().strip().strip('v')
return version
def check_release():
- release = None
try:
release = _get_version()
- except (FileNotFoundError, Exception):
- pass
- if release:
- assert release == __version__, 'Current version does not match with manifest VERSION'
+ except FileNotFoundError:
+ return
+
+ assert release == __version__, 'Current version does not match with manifest VERSION'
check_release()
diff --git a/comment/admin.py b/comment/admin.py
index 7cce37f..edc6855 100644
--- a/comment/admin.py
+++ b/comment/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from comment.models import Comment, Flag, FlagInstance, Reaction, ReactionInstance
+from comment.models import Comment, Flag, FlagInstance, Reaction, ReactionInstance, Follower
class CommentModelAdmin(admin.ModelAdmin):
@@ -37,6 +37,13 @@ class FlagModelAdmin(admin.ModelAdmin):
inlines = [InlineFlagInstance]
+class FollowerModelAdmin(admin.ModelAdmin):
+ list_display = ('email', 'username', 'content_type', 'content_object')
+ readonly_fields = list_display
+ search_fields = ('email',)
+
+
admin.site.register(Comment, CommentModelAdmin)
admin.site.register(Reaction, ReactionModelAdmin)
admin.site.register(Flag, FlagModelAdmin)
+admin.site.register(Follower, FollowerModelAdmin)
diff --git a/comment/api/permissions.py b/comment/api/permissions.py
index da6c6ea..497b4d1 100644
--- a/comment/api/permissions.py
+++ b/comment/api/permissions.py
@@ -36,3 +36,18 @@ def has_permission(self, request, view):
def has_object_permission(self, request, view, obj):
return obj.is_flagged and (is_comment_admin(request.user) or is_comment_moderator(request.user))
+
+
+class SubscriptionEnabled(permissions.BasePermission):
+ """
+ This will check if the COMMENT_ALLOW_SUBSCRIPTION is enabled
+ """
+ def has_permission(self, request, view):
+ return getattr(settings, 'COMMENT_ALLOW_SUBSCRIPTION', False)
+
+
+class CanGetSubscribers(SubscriptionEnabled):
+ def has_permission(self, request, view):
+ if not super().has_permission(request, view):
+ return False
+ return is_comment_admin(request.user) or is_comment_moderator(request.user)
diff --git a/comment/api/serializers.py b/comment/api/serializers.py
index 547638c..70081b1 100644
--- a/comment/api/serializers.py
+++ b/comment/api/serializers.py
@@ -7,8 +7,9 @@
from comment.conf import settings
from comment.models import Comment, Flag, Reaction
-from comment.utils import process_anonymous_commenting, get_user_for_request, get_profile_instance
+from comment.utils import get_user_for_request, get_profile_instance
from comment.messages import EmailError
+from comment.service.email import DABEmailService
def get_profile_model():
@@ -97,6 +98,7 @@ class Meta:
def __init__(self, *args, **kwargs):
user = kwargs['context']['request'].user
+ self.email_service = None
if user.is_authenticated or not settings.COMMENT_ALLOW_ANONYMOUS:
del self.fields['email']
@@ -122,11 +124,14 @@ def create(self, validated_data):
parent=self.context['parent_comment'],
email=email,
posted=time_posted
- )
+ )
+ self.email_service = DABEmailService(comment, request)
if settings.COMMENT_ALLOW_ANONYMOUS and not user:
- process_anonymous_commenting(request, comment, api=True)
+ self.email_service.send_confirmation_request(api=True)
else:
comment.save()
+ if settings.COMMENT_ALLOW_SUBSCRIPTION:
+ self.email_service.send_notification_to_followers()
return comment
diff --git a/comment/api/urls.py b/comment/api/urls.py
index 2da71bb..956f0b0 100644
--- a/comment/api/urls.py
+++ b/comment/api/urls.py
@@ -15,6 +15,8 @@
name='comments-flag-state-change'
),
re_path(r'^comments/confirm/(?P[^/]+)/$', views.ConfirmComment.as_view(), name='confirm-comment'),
+ path('comments/toggle-subscription/', views.ToggleFollowAPI.as_view(), name='toggle-subscription'),
+ path('comments/subscribers/', views.SubscribersAPI.as_view(), name='subscribers'),
]
urlpatterns = format_suffix_patterns(urlpatterns)
diff --git a/comment/api/views.py b/comment/api/views.py
index 09a7f3f..c275b1d 100644
--- a/comment/api/views.py
+++ b/comment/api/views.py
@@ -4,19 +4,21 @@
from rest_framework.response import Response
from rest_framework.views import APIView
+from comment.conf import settings
from comment.validators import ValidatorMixin, ContentTypeValidator
from comment.api.serializers import CommentSerializer, CommentCreateSerializer
from comment.api.permissions import (
- IsOwnerOrReadOnly, FlagEnabledPermission, CanChangeFlaggedCommentState
-)
-from comment.models import Comment, Reaction, ReactionInstance, Flag, FlagInstance
+ IsOwnerOrReadOnly, FlagEnabledPermission, CanChangeFlaggedCommentState,
+ SubscriptionEnabled, CanGetSubscribers)
+from comment.models import Comment, Reaction, ReactionInstance, Flag, FlagInstance, Follower
from comment.utils import get_comment_from_key, CommentFailReason
from comment.messages import FlagError, EmailError
+from comment.views import BaseToggleFollowView
+from comment.service.email import DABEmailService
class CommentCreate(ValidatorMixin, generics.CreateAPIView):
serializer_class = CommentCreateSerializer
- permission_classes = ()
api = True
def get_serializer_context(self):
@@ -131,15 +133,46 @@ def post(self, request, *args, **kwargs):
class ConfirmComment(APIView):
- @staticmethod
- def get(request, *args, **kwargs):
+ email_service = None
+
+ def get(self, request, *args, **kwargs):
key = kwargs.get('key', None)
- comment = get_comment_from_key(key)
+ temp_comment = get_comment_from_key(key)
- if comment.why_invalid == CommentFailReason.BAD:
+ if temp_comment.why_invalid == CommentFailReason.BAD:
return Response({'detail': EmailError.BROKEN_VERIFICATION_LINK}, status=status.HTTP_400_BAD_REQUEST)
- if comment.why_invalid == CommentFailReason.EXISTS:
+ if temp_comment.why_invalid == CommentFailReason.EXISTS:
return Response({'detail': EmailError.USED_VERIFICATION_LINK}, status=status.HTTP_200_OK)
- return Response(CommentSerializer(comment.obj).data, status=status.HTTP_201_CREATED)
+ comment = temp_comment.obj
+ comment.save()
+ comment.refresh_from_db()
+ if settings.COMMENT_ALLOW_SUBSCRIPTION:
+ self.email_service = DABEmailService(comment, request)
+ self.email_service.send_notification_to_followers()
+ return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
+
+
+class ToggleFollowAPI(BaseToggleFollowView, APIView):
+ api = True
+ response_class = Response
+ permission_classes = (SubscriptionEnabled, permissions.IsAuthenticated)
+
+ def post(self, request, *args, **kwargs):
+ self.validate(request)
+ return super().post(request, *args, **kwargs)
+
+
+class SubscribersAPI(ContentTypeValidator, APIView):
+ api = True
+ permission_classes = (CanGetSubscribers,)
+
+ def get(self, request, *args, **kwargs):
+ self.validate(request)
+ return Response({
+ 'app_name': self.model_obj._meta.app_label,
+ 'model_name': self.model_obj.__class__.__name__,
+ 'model_id': self.model_obj.id,
+ 'followers': Follower.objects.get_emails_for_model_object(self.model_obj)
+ })
diff --git a/comment/conf/defaults.py b/comment/conf/defaults.py
index 262609a..19d408d 100644
--- a/comment/conf/defaults.py
+++ b/comment/conf/defaults.py
@@ -26,3 +26,5 @@
COMMENT_USE_EMAIL_FIRST_PART_AS_USERNAME = False
COMMENT_ALLOW_TRANSLATION = False
+
+COMMENT_ALLOW_SUBSCRIPTION = False
diff --git a/comment/locale/hi/LC_MESSAGES/django.po b/comment/locale/hi/LC_MESSAGES/django.po
index e46e965..28cab45 100644
--- a/comment/locale/hi/LC_MESSAGES/django.po
+++ b/comment/locale/hi/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-11-24 06:40+0000\n"
+"POT-Creation-Date: 2020-12-22 08:52+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ABHYUDAI \n"
"Language-Team: LANGUAGE \n"
@@ -17,6 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
#: comment/apps.py:8
msgid "comment"
msgstr "टिप्पणी"
@@ -42,7 +43,9 @@ msgstr "Comment ऐप: LOGIN_URL सेटिंग में नहीं ह
msgid ""
"Your {class_name} class has not defined a {method_name} method, which is "
"required."
-msgstr "आपकी {class_name} class ने एक {method_name} विधि(method) को परिभाषित नहीं किया है, जिसकी आवश्यकता है"
+msgstr ""
+"आपकी {class_name} class ने एक {method_name} विधि(method) को परिभाषित नहीं किया "
+"है, जिसकी आवश्यकता है"
#: comment/messages.py:7
msgid "Only AJAX request are allowed"
@@ -93,7 +96,9 @@ msgstr "{model_id} मॉडल के लिए मान्य मॉडल
msgid ""
"{parent_id} is NOT a valid id for a parent comment or the parent comment "
"does NOT belong to the provided model object"
-msgstr "{parent_id} मूल टिप्पणी के लिए मान्य आईडी नहीं है या मूल टिप्पणी प्रदान की गई मॉडल ऑब्जेक्ट से संबंधित नहीं है"
+msgstr ""
+"{parent_id} मूल टिप्पणी के लिए मान्य आईडी नहीं है या मूल टिप्पणी प्रदान की गई मॉडल "
+"ऑब्जेक्ट से संबंधित नहीं है"
#: comment/messages.py:28
msgid "Flagging system must be enabled"
@@ -174,7 +179,9 @@ msgstr "टिप्पणी पुष्टि अनुरोध"
msgid ""
"We have sent a verification link to your email.The comment will be displayed "
"after it is verified."
-msgstr "हमने आपके ईमेल पर एक सत्यापन लिंक भेजा है। सत्यापित होने के बाद टिप्पणी प्रदर्शित की जाएगी।"
+msgstr ""
+"हमने आपके ईमेल पर एक सत्यापन लिंक भेजा है। सत्यापित होने के बाद टिप्पणी प्रदर्शित की "
+"जाएगी।"
#: comment/messages.py:62
msgid "email address, this will be used for verification."
@@ -185,70 +192,101 @@ msgid "email address, it will be used for verification."
msgstr "ईमेल पता, इसका उपयोग सत्यापन के लिए किया जाएगा।"
#: comment/messages.py:64
+#, python-brace-format
+msgid "{username} added comment to \"{thread_name}\""
+msgstr "{username} में टिप्पणी जोड़ दी \"{thread_name}\""
+
+#: comment/messages.py:65
msgid "email"
msgstr "ईमेल"
-#: comment/messages.py:68
+#: comment/messages.py:69
msgid "Unflagged"
msgstr "फ्लैग नहीं किया गया है"
-#: comment/messages.py:69
+#: comment/messages.py:70
msgid "Flagged"
msgstr "फ्लैग किया गया है"
-#: comment/messages.py:70
+#: comment/messages.py:71
msgid "Flag rejected by the moderator"
msgstr "मॉडरेटर द्वारा फ्लैग को अस्वीकार कर दिया गया"
-#: comment/messages.py:71
+#: comment/messages.py:72
msgid "Comment modified by the author"
msgstr "टिप्पणी को लेखक द्वारा संशोधित किया गया है"
-#: comment/templates/comment/anonymous/confirmation_request.html:3
+#: comment/messages.py:76
+msgid "Please insert a valid email"
+msgstr "कृपया एक मान्य ईमेल डालें"
+
+#: comment/messages.py:77
+#, python-brace-format
+msgid "Email is required to subscribe {model_object}"
+msgstr "सदस्यता लेने के लिए ईमेल आवश्यक है {model_object}"
+
+#: comment/messages.py:78
+msgid "Subscribe system must be enabled"
+msgstr "सदस्यता प्रणाली सक्षम होना चाहिए"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:8
#: comment/templates/comment/anonymous/confirmation_request.txt:2
-msgid "Hey,"
-msgstr "नमस्ते"
+msgid "Hey there,"
+msgstr "सुनो,"
-#: comment/templates/comment/anonymous/confirmation_request.html:5
+#: comment/templates/comment/anonymous/confirmation_request.html:10
msgid ""
-"You or someone on behalf of you have requested to post a comment into this "
+"You or someone on your behalf have requested to post a comment into this "
"page:"
-msgstr "आप या आपकी ओर से किसी ने इस पृष्ठ पर एक टिप्पणी पोस्ट करने का अनुरोध किया है"
+msgstr "आपने या आपकी ओर से किसी ने इस पर टिप्पणी पोस्ट करने का अनुरोध किया है "
+"पृष्ठ:"
-#: comment/templates/comment/anonymous/confirmation_request.html:6
+#: comment/templates/comment/anonymous/confirmation_request.html:12
#, python-format
msgid "at %(posted_time)s."
msgstr "%(posted_time)s पर।"
-#: comment/templates/comment/anonymous/confirmation_request.html:9
+#: comment/templates/comment/anonymous/confirmation_request.html:15
+#: comment/templates/comment/notifications/notification.html:27
msgid "The comment:"
msgstr "टिप्पणी:"
-#: comment/templates/comment/anonymous/confirmation_request.html:13
+#: comment/templates/comment/anonymous/confirmation_request.html:20
#: comment/templates/comment/anonymous/confirmation_request.txt:12
msgid ""
"If you do not wish to post the comment, please ignore this message or report "
"an incident to"
-msgstr "यदि आप टिप्पणी पोस्ट करने की इच्छा नहीं रखते हैं, कृपया इस संदेश को अनदेखा करें या घटना की रिपोर्ट करें"
+msgstr ""
+"यदि आप टिप्पणी पोस्ट करने की इच्छा नहीं रखते हैं, कृपया इस संदेश को अनदेखा करें या घटना की "
+"रिपोर्ट करें"
-#: comment/templates/comment/anonymous/confirmation_request.html:13
-msgid "Otherwise click on the link below to confirm the comment."
-msgstr "अन्यथा टिप्पणी की पुष्टि करने के लिए नीचे दिए गए लिंक पर क्लिक करें।"
+#: comment/templates/comment/anonymous/confirmation_request.html:20
+msgid "Otherwise click on the button below to confirm the comment."
+msgstr "अन्यथा टिप्पणी की पुष्टि करने के लिए नीचे दिए गए बटन पर क्लिक करें।"
-#: comment/templates/comment/anonymous/confirmation_request.html:17
-#: comment/templates/comment/anonymous/confirmation_request.txt:16
+#: comment/templates/comment/anonymous/confirmation_request.html:29
+msgid "Confirm your comment"
+msgstr "अपनी टिप्पणी की पुष्टि करें"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:38
msgid ""
-"If clicking does not work, you can also copy and paste the address into your "
-"browser's address window"
-msgstr "यदि क्लिक करने से काम नहीं चलता है, आप अपने ब्राउज़र की विंडो में लिंक को कॉपी और पेस्ट भी कर सकते हैं"
+"If clicking does not work, you can also copy the below link and paste the "
+"address into your browser's address window"
+msgstr ""
+"यदि क्लिक करने से काम नहीं चलता है, तो आप नीचे दिए गए लिंक को भी कॉपी कर सकते हैं और पेस्ट कर सकते हैं "
+"आपके ब्राउज़र की पता विंडो में पता"
-#: comment/templates/comment/anonymous/confirmation_request.html:19
+#: comment/templates/comment/anonymous/confirmation_request.html:41
#: comment/templates/comment/anonymous/confirmation_request.txt:17
+#: comment/templates/comment/notifications/notification.html:32
+#: comment/templates/comment/notifications/notification.txt:14
msgid "Thanks for your comment!"
msgstr "आपके टिप्पणी के लिए धन्यवाद!"
-#: comment/templates/comment/anonymous/confirmation_request.html:21
+#: comment/templates/comment/anonymous/confirmation_request.html:43
#: comment/templates/comment/anonymous/confirmation_request.txt:20
+#: comment/templates/comment/notifications/notification.html:34
+#: comment/templates/comment/notifications/notification.txt:17
msgid "Kind regards,"
msgstr "सधन्यवाद,"
@@ -262,6 +300,7 @@ msgstr "निम्न URL पर"
#: comment/templates/comment/anonymous/confirmation_request.txt:8
#: comment/templates/comment/comments/create_comment.html:20
+#: comment/templates/comment/notifications/notification.txt:6
msgid "Comment"
msgstr "टिप्पणी"
@@ -269,6 +308,16 @@ msgstr "टिप्पणी"
msgid "Otherwise click on the link below to confirm the comment"
msgstr "अन्यथा टिप्पणी की पुष्टि करने के लिए नीचे दिए गए लिंक पर क्लिक करें"
+#: comment/templates/comment/anonymous/confirmation_request.txt:16
+#: comment/templates/comment/notifications/notification.html:31
+#: comment/templates/comment/notifications/notification.txt:13
+msgid ""
+"If clicking does not work, you can also copy and paste the address into your "
+"browser's address window"
+msgstr ""
+"यदि क्लिक करने से काम नहीं चलता है, आप अपने ब्राउज़र की विंडो में लिंक को कॉपी और पेस्ट भी "
+"कर सकते हैं"
+
#: comment/templates/comment/anonymous/discarded.html:7
msgid "Comment discarded"
msgstr "टिप्पणी अस्वीकृत कर दी गई है"
@@ -285,34 +334,34 @@ msgstr "क्षमा करें, आपकी टिप्पणी स्
msgid "Comments"
msgstr "टिप्पणियाँ"
-#: comment/templates/comment/comments/comment_content.html:14
+#: comment/templates/comment/comments/comment_content.html:13
#, python-format
msgid "view %(comment.user)s profile"
msgstr "देखें %(comment.user)s का प्रोफ़ाइल"
-#: comment/templates/comment/comments/comment_content.html:14
+#: comment/templates/comment/comments/comment_content.html:13
msgid "comment by anonymous user"
msgstr "गुमनाम उपयोगकर्ता द्वारा टिप्पणी"
-#: comment/templates/comment/comments/comment_content.html:19
+#: comment/templates/comment/comments/comment_content.html:18
msgid "Edited"
msgstr "बदला हुआ"
-#: comment/templates/comment/comments/comment_content.html:19
+#: comment/templates/comment/comments/comment_content.html:18
#, python-format
msgid "Edited: %(edited_time)s ago"
msgstr "बदला हुआ: %(edited_time)s पहले"
-#: comment/templates/comment/comments/comment_content.html:21
+#: comment/templates/comment/comments/comment_content.html:20
msgid "Posted"
msgstr "पोस्ट किया गया है"
-#: comment/templates/comment/comments/comment_content.html:21
+#: comment/templates/comment/comments/comment_content.html:20
#, python-format
msgid "%(posted_time)s ago"
msgstr "%(posted_time)s पहले"
-#: comment/templates/comment/comments/comment_content.html:40
+#: comment/templates/comment/comments/comment_content.html:39
#, python-format
msgid " Repl%(plural_str)s "
msgstr "जवाब %(plural_str)s"
@@ -324,6 +373,7 @@ msgstr "टिप्पणी हटाने की पुष्टि कर
#: comment/templates/comment/comments/comment_modal.html:7
#: comment/templates/comment/comments/comment_modal.html:15
#: comment/templates/comment/flags/flag_modal.html:8
+#: comment/templates/comment/follow/follow_modal.html:8
msgid "Close"
msgstr "बंद करे"
@@ -428,6 +478,50 @@ msgstr "फ्लैग"
msgid "flag"
msgstr "फ्लैग"
+#: comment/templates/comment/follow/follow.html:9
+msgid "follow"
+msgstr "का पालन करें"
+
+#: comment/templates/comment/follow/follow_icon.html:3
+msgid "Unfollow this thread"
+msgstr "इस धागे को सामने रखें"
+
+#: comment/templates/comment/follow/follow_icon.html:3
+msgid "Follow this thread"
+msgstr "इस सूत्र का पालन करें"
+
+#: comment/templates/comment/follow/follow_modal.html:7
+msgid "Please insert your email to follow this thread"
+msgstr "कृपया इस धागे का पालन करने के लिए अपना ईमेल डालें"
+
+#: comment/templates/comment/follow/follow_modal.html:25
+msgid "Follow"
+msgstr "का पालन करें"
+
+#: comment/templates/comment/notifications/notification.html:6
+#: comment/templates/comment/notifications/notification.txt:2
+msgid "Hey"
+msgstr "नमस्ते"
+
+#: comment/templates/comment/notifications/notification.html:6
+#: comment/templates/comment/notifications/notification.txt:2
+msgid ","
+msgstr ""
+
+#: comment/templates/comment/notifications/notification.html:9
+#: comment/templates/comment/notifications/notification.txt:4
+msgid "has added a comment at"
+msgstr "पर एक टिप्पणी जोड़ी गई है"
+
+#: comment/templates/comment/notifications/notification.html:9
+#: comment/templates/comment/notifications/notification.txt:4
+msgid "to"
+msgstr "सेवा"
+
+#: comment/templates/comment/notifications/notification.html:18
+msgid "Go to site"
+msgstr "साइट पर जाएं"
+
#: comment/templates/comment/reactions/reactions.html:5
msgid "like"
msgstr "पसंद"
diff --git a/comment/locale/hi/LC_MESSAGES/djangojs.po b/comment/locale/hi/LC_MESSAGES/djangojs.po
index f7502f1..8bdf813 100644
--- a/comment/locale/hi/LC_MESSAGES/djangojs.po
+++ b/comment/locale/hi/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-11-24 08:38+0000\n"
+"POT-Creation-Date: 2020-12-13 13:43+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ABHYUDAI \n"
"Language-Team: LANGUAGE \n"
@@ -17,58 +17,63 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: comment/static/js/comment.js:145 comment/static/js/comment.js:286
+
+#: comment/static/js/comment.js:151 comment/static/js/comment.js:291
msgid "Replies"
msgstr "जवाब"
-#: comment/static/js/comment.js:147 comment/static/js/comment.js:288
+#: comment/static/js/comment.js:153 comment/static/js/comment.js:293
msgid "Reply"
msgstr "जवाब दें"
-#: comment/static/js/comment.js:164
+#: comment/static/js/comment.js:170
msgid "Unable to post your comment!, please try again"
msgstr "आपकी टिप्पणी पोस्ट करने में असमर्थ!, कृपया पुनः प्रयास करें"
-#: comment/static/js/comment.js:185
+#: comment/static/js/comment.js:191
msgid "You can't edit this comment"
msgstr "आप इस टिप्पणी को बदल नहीं सकते"
-#: comment/static/js/comment.js:208
+#: comment/static/js/comment.js:214
msgid "Modification didn't take effect!, please try again"
msgstr "संशोधन प्रभावी नहीं हुआ!, कृपया पुनः प्रयास करें"
-#: comment/static/js/comment.js:298
+#: comment/static/js/comment.js:302
msgid "Unable to delete your comment!, please try again"
msgstr "आपकी टिप्पणी को हटाने में असमर्थ!, कृपया पुनः प्रयास करें"
-#: comment/static/js/comment.js:355
+#: comment/static/js/comment.js:359
msgid "Reaction couldn't be processed!, please try again"
msgstr "प्रतिक्रिया संसाधित नहीं की जा सकी!, कृपया पुनः प्रयास करें"
-#: comment/static/js/comment.js:462
+#: comment/static/js/comment.js:394
+msgid "Subscription couldn't be processed!, please try again"
+msgstr "सदस्यता संसाधित नहीं हो सकी!, कृपया पुनः प्रयास करें"
+
+#: comment/static/js/comment.js:500
msgid "Flagging couldn't be processed!, please try again"
msgstr "फ़्लैगिंग को संसाधित नहीं किया जा सका!, कृपया पुनः प्रयास करें"
-#: comment/static/js/comment.js:508
+#: comment/static/js/comment.js:545
msgid "read more ..."
msgstr "और पढ़ें ..."
-#: comment/static/js/comment.js:510
+#: comment/static/js/comment.js:547
msgid "read less"
msgstr "कम पढ़ें"
-#: comment/static/js/comment.js:538
+#: comment/static/js/comment.js:575
msgid "Flag rejected"
msgstr "फ्लैग को अस्वीकार कर दिया गया"
-#: comment/static/js/comment.js:539 comment/static/js/comment.js:557
+#: comment/static/js/comment.js:576 comment/static/js/comment.js:594
msgid "Reject the flag"
msgstr "फ्लैग को अस्वीकार करें"
-#: comment/static/js/comment.js:544 comment/static/js/comment.js:552
+#: comment/static/js/comment.js:581 comment/static/js/comment.js:589
msgid "Resolve the flag"
msgstr "फ्लैग को समाधान करें"
-#: comment/static/js/comment.js:551
+#: comment/static/js/comment.js:588
msgid "Flag resolved"
msgstr "फ्लैग का समाधान कर दिया गया है"
diff --git a/comment/locale/test/LC_MESSAGES/django.po b/comment/locale/test/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4be95ec
--- /dev/null
+++ b/comment/locale/test/LC_MESSAGES/django.po
@@ -0,0 +1,517 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-12-25 15:41+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: comment/apps.py:8
+msgid "comment"
+msgstr"über"
+
+#: comment/conf/defaults.py:13
+msgid "Spam | Exists only to promote a service"
+msgstr"über"
+
+#: comment/conf/defaults.py:14
+msgid "Abusive | Intended at promoting hatred"
+msgstr"über"
+
+#: comment/managers/flags.py:23
+msgid "Something else"
+msgstr"über"
+
+#: comment/messages.py:5
+msgid "Comment App: LOGIN_URL is not in the settings"
+msgstr"über"
+
+#: comment/messages.py:6
+#, python-brace-format
+msgid ""
+"Your {class_name} class has not defined a {method_name} method, which is "
+"required."
+msgstr"{class_name} über {method_name}"
+
+#: comment/messages.py:7
+msgid "Only AJAX request are allowed"
+msgstr"über"
+
+#: comment/messages.py:11
+msgid "error"
+msgstr"über"
+
+#: comment/messages.py:12
+msgid "Bad Request"
+msgstr"über"
+
+#: comment/messages.py:16
+#, python-brace-format
+msgid "{var_name} id must be an integer, {id} is NOT"
+msgstr"{var_name} über {id}"
+
+#: comment/messages.py:17
+msgid "app name must be provided"
+msgstr"über"
+
+#: comment/messages.py:18
+#, python-brace-format
+msgid "{app_name} is NOT a valid app name"
+msgstr"{app_name} über"
+
+#: comment/messages.py:19
+msgid "model name must be provided"
+msgstr"über"
+
+#: comment/messages.py:20
+#, python-brace-format
+msgid "{model_name} is NOT a valid model name"
+msgstr"{model_name} über"
+
+#: comment/messages.py:21
+msgid "model id must be provided"
+msgstr"über"
+
+#: comment/messages.py:22
+#, python-brace-format
+msgid "{model_id} is NOT a valid model id for the model {model_name}"
+msgstr"{model_id} über {model_name}"
+
+#: comment/messages.py:23
+#, python-brace-format
+msgid ""
+"{parent_id} is NOT a valid id for a parent comment or the parent comment "
+"does NOT belong to the provided model object"
+msgstr"{parent_id} über"
+
+#: comment/messages.py:28
+msgid "Flagging system must be enabled"
+msgstr"über"
+
+#: comment/messages.py:29
+msgid "Object must be flagged!"
+msgstr"über"
+
+#: comment/messages.py:30
+#, python-brace-format
+msgid "{state} is an invalid state"
+msgstr"{state} über"
+
+#: comment/messages.py:31
+#, python-brace-format
+msgid "{reason} is an invalid reason"
+msgstr"{reason} über"
+
+#: comment/messages.py:32
+msgid "Please supply some information as the reason for flagging"
+msgstr"über"
+
+#: comment/messages.py:33
+#, python-brace-format
+msgid "This comment is already flagged by this user ({user})"
+msgstr"über ({user})"
+
+#: comment/messages.py:34
+#, python-brace-format
+msgid "This comment was not flagged by this user ({user})"
+msgstr"über ({user})"
+
+#: comment/messages.py:35
+msgid "This action cannot be applied on unflagged comments"
+msgstr"über"
+
+#: comment/messages.py:36
+msgid "The comment must be edited before resolving the flag"
+msgstr"über"
+
+#: comment/messages.py:40
+#, python-brace-format
+msgid ""
+"Reaction must be an valid ReactionManager.RelationType. {reaction_type} is "
+"not"
+msgstr"über {reaction_type}"
+
+#: comment/messages.py:44
+msgid "Email is required for posting anonymous comments."
+msgstr"über"
+
+#: comment/messages.py:45
+msgid "The link seems to be broken."
+msgstr"über"
+
+#: comment/messages.py:46
+msgid "The comment has already been verified."
+msgstr"über"
+
+#: comment/messages.py:50
+msgid "Comment flagged"
+msgstr"über"
+
+#: comment/messages.py:51
+msgid "Comment flag removed"
+msgstr"über"
+
+#: comment/messages.py:55
+msgid "Your reaction has been updated successfully"
+msgstr"über"
+
+#: comment/messages.py:59
+msgid "Comment Confirmation Request"
+msgstr"über"
+
+#: comment/messages.py:60
+msgid ""
+"We have sent a verification link to your email.The comment will be displayed "
+"after it is verified."
+msgstr"über"
+
+#: comment/messages.py:62
+msgid "email address, this will be used for verification."
+msgstr"über"
+
+#: comment/messages.py:63
+msgid "email address, it will be used for verification."
+msgstr"über"
+
+#: comment/messages.py:64
+#, python-brace-format
+msgid "{username} added comment to \"{thread_name}\""
+msgstr"{username} über \"{thread_name}\""
+
+#: comment/messages.py:65
+msgid "email"
+msgstr"über"
+
+#: comment/messages.py:69
+msgid "Unflagged"
+msgstr"über"
+
+#: comment/messages.py:70
+msgid "Flagged"
+msgstr"über"
+
+#: comment/messages.py:71
+msgid "Flag rejected by the moderator"
+msgstr"über"
+
+#: comment/messages.py:72
+msgid "Comment modified by the author"
+msgstr"über"
+
+#: comment/messages.py:76
+msgid "Please insert a valid email"
+msgstr"über"
+
+#: comment/messages.py:77
+#, python-brace-format
+msgid "Email is required to subscribe {model_object}"
+msgstr"über {model_object}"
+
+#: comment/messages.py:78
+msgid "Subscribe system must be enabled"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:8
+#: comment/templates/comment/anonymous/confirmation_request.txt:2
+msgid "Hey there,"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:10
+msgid ""
+"You or someone on your behalf have requested to post a comment into this "
+"page:"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:12
+#, python-format
+msgid "at %(posted_time)s."
+msgstr"über %(posted_time)s."
+
+#: comment/templates/comment/anonymous/confirmation_request.html:15
+#: comment/templates/comment/notifications/notification.html:27
+msgid "The comment:"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:20
+#: comment/templates/comment/anonymous/confirmation_request.txt:12
+msgid ""
+"If you do not wish to post the comment, please ignore this message or report "
+"an incident to"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:20
+msgid "Otherwise click on the button below to confirm the comment."
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:29
+msgid "Confirm your comment"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:38
+msgid ""
+"If clicking does not work, you can also copy the below link and paste the "
+"address into your browser's address window"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:41
+#: comment/templates/comment/anonymous/confirmation_request.txt:17
+#: comment/templates/comment/notifications/notification.html:32
+#: comment/templates/comment/notifications/notification.txt:14
+msgid "Thanks for your comment!"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.html:43
+#: comment/templates/comment/anonymous/confirmation_request.txt:20
+#: comment/templates/comment/notifications/notification.html:34
+#: comment/templates/comment/notifications/notification.txt:17
+msgid "Kind regards,"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.txt:4
+msgid "You or someone on behalf of you have requested to post a comment at"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.txt:4
+msgid "to the following URL"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.txt:8
+#: comment/templates/comment/comments/create_comment.html:20
+#: comment/templates/comment/notifications/notification.txt:6
+msgid "Comment"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.txt:12
+msgid "Otherwise click on the link below to confirm the comment"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/confirmation_request.txt:16
+#: comment/templates/comment/notifications/notification.html:31
+#: comment/templates/comment/notifications/notification.txt:13
+msgid ""
+"If clicking does not work, you can also copy and paste the address into your "
+"browser's address window"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/discarded.html:7
+msgid "Comment discarded"
+msgstr"über"
+
+#: comment/templates/comment/anonymous/discarded.html:13
+msgid "Comment can't be verified."
+msgstr"über"
+
+#: comment/templates/comment/anonymous/discarded.html:14
+msgid "Sorry, your comment has been automatically discarded."
+msgstr"über"
+
+#: comment/templates/comment/comments/base.html:5
+msgid "Comments"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_content.html:13
+#, python-format
+msgid "view %(comment.user)s profile"
+msgstr"%(comment.user)s über"
+
+#: comment/templates/comment/comments/comment_content.html:13
+msgid "comment by anonymous user"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_content.html:18
+msgid "Edited"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_content.html:18
+#, python-format
+msgid "Edited: %(edited_time)s ago"
+msgstr"%(edited_time)s über"
+
+#: comment/templates/comment/comments/comment_content.html:20
+msgid "Posted"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_content.html:20
+#, python-format
+msgid "%(posted_time)s ago"
+msgstr"%(posted_time)s über"
+
+#: comment/templates/comment/comments/comment_content.html:39
+#, python-format
+msgid " Repl%(plural_str)s "
+msgstr"über%(plural_str)s"
+
+#: comment/templates/comment/comments/comment_modal.html:6
+msgid "Confirm comment deletion"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_modal.html:7
+#: comment/templates/comment/comments/comment_modal.html:15
+#: comment/templates/comment/flags/flag_modal.html:8
+#: comment/templates/comment/follow/follow_modal.html:8
+msgid "Close"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_modal.html:12
+msgid "Are you sure you want to delete this comment"
+msgstr"über"
+
+#: comment/templates/comment/comments/comment_modal.html:23
+msgid "Delete"
+msgstr"über"
+
+#: comment/templates/comment/comments/content.html:9
+msgid "read more"
+msgstr"über"
+
+#: comment/templates/comment/comments/create_comment.html:20
+msgid "Reply"
+msgstr"über"
+
+#: comment/templates/comment/comments/create_comment.html:29
+msgid "Comment Anonymously or"
+msgstr"über"
+
+#: comment/templates/comment/comments/create_comment.html:31
+#: comment/templates/comment/comments/create_comment.html:37
+msgid "Login"
+msgstr"über"
+
+#: comment/templates/comment/comments/create_comment.html:33
+msgid "to keep your conversations intact"
+msgstr"über"
+
+#: comment/templates/comment/comments/create_comment.html:39
+msgid "to join the discussion"
+msgstr"über"
+
+#: comment/templates/comment/comments/delete_icon.html:3
+msgid "Delete comment"
+msgstr"über"
+
+#: comment/templates/comment/comments/edit_icon.html:3
+msgid "Edit comment"
+msgstr"über"
+
+#: comment/templates/comment/comments/pagination.html:2
+msgid "Page navigation"
+msgstr"über"
+
+#: comment/templates/comment/comments/pagination.html:3
+msgid "first"
+msgstr"über"
+
+#: comment/templates/comment/comments/pagination.html:4
+msgid "last"
+msgstr"über"
+
+#: comment/templates/comment/comments/pagination.html:5
+msgid "Next"
+msgstr"über"
+
+#: comment/templates/comment/comments/pagination.html:6
+msgid "Previous"
+msgstr"über"
+
+#: comment/templates/comment/comments/reject_icon.html:3
+msgid "Flag rejected"
+msgstr"über"
+
+#: comment/templates/comment/comments/reject_icon.html:3
+msgid "Reject the flag"
+msgstr"über"
+
+#: comment/templates/comment/comments/resolve_icon.html:3
+msgid "Flag resolved"
+msgstr"über"
+
+#: comment/templates/comment/comments/resolve_icon.html:3
+msgid "Resolve the flag"
+msgstr"über"
+
+#: comment/templates/comment/comments/urlhash.html:2
+msgid "Permalink to this comment"
+msgstr"über"
+
+#: comment/templates/comment/flags/flag_icon.html:3
+msgid "Remove flag"
+msgstr"über"
+
+#: comment/templates/comment/flags/flag_icon.html:3
+msgid "Report comment"
+msgstr"über"
+
+#: comment/templates/comment/flags/flag_modal.html:7
+msgid "Please select a reason for flagging"
+msgstr"über"
+
+#: comment/templates/comment/flags/flag_modal.html:22
+msgid "Flag"
+msgstr"über"
+
+#: comment/templates/comment/flags/flags.html:8
+msgid "flag"
+msgstr"über"
+
+#: comment/templates/comment/follow/follow.html:9
+msgid "follow"
+msgstr"über"
+
+#: comment/templates/comment/follow/follow_icon.html:3
+msgid "Unfollow this thread"
+msgstr"über"
+
+#: comment/templates/comment/follow/follow_icon.html:3
+msgid "Follow this thread"
+msgstr"über"
+
+#: comment/templates/comment/follow/follow_modal.html:7
+msgid "Please insert your email to follow this thread"
+msgstr"über"
+
+#: comment/templates/comment/follow/follow_modal.html:28
+msgid "Follow"
+msgstr"über"
+
+#: comment/templates/comment/notifications/notification.html:6
+#: comment/templates/comment/notifications/notification.txt:2
+msgid "Hey"
+msgstr"über"
+
+#: comment/templates/comment/notifications/notification.html:6
+#: comment/templates/comment/notifications/notification.txt:2
+msgid ","
+msgstr"über"
+
+#: comment/templates/comment/notifications/notification.html:9
+#: comment/templates/comment/notifications/notification.txt:4
+msgid "has added a comment at"
+msgstr"über"
+
+#: comment/templates/comment/notifications/notification.html:9
+#: comment/templates/comment/notifications/notification.txt:4
+msgid "to"
+msgstr"über"
+
+#: comment/templates/comment/notifications/notification.html:18
+msgid "Go to site"
+msgstr"über"
+
+#: comment/templates/comment/reactions/reactions.html:5
+msgid "like"
+msgstr"über"
+
+#: comment/templates/comment/reactions/reactions.html:16
+msgid "dislike"
+msgstr"über"
diff --git a/comment/locale/test/LC_MESSAGES/djangojs.po b/comment/locale/test/LC_MESSAGES/djangojs.po
new file mode 100644
index 0000000..8b26f41
--- /dev/null
+++ b/comment/locale/test/LC_MESSAGES/djangojs.po
@@ -0,0 +1,78 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-12-25 15:41+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: comment/static/js/comment.js:153 comment/static/js/comment.js:297
+msgid "Replies"
+msgstr"über"
+
+#: comment/static/js/comment.js:155 comment/static/js/comment.js:299
+msgid "Reply"
+msgstr"über"
+
+#: comment/static/js/comment.js:176
+msgid "Unable to post your comment!, please try again"
+msgstr"über"
+
+#: comment/static/js/comment.js:197
+msgid "You can't edit this comment"
+msgstr"über"
+
+#: comment/static/js/comment.js:220
+msgid "Modification didn't take effect!, please try again"
+msgstr"über"
+
+#: comment/static/js/comment.js:308
+msgid "Unable to delete your comment!, please try again"
+msgstr"über"
+
+#: comment/static/js/comment.js:365
+msgid "Reaction couldn't be processed!, please try again"
+msgstr"über"
+
+#: comment/static/js/comment.js:400
+msgid "Subscription couldn't be processed!, please try again"
+msgstr"über"
+
+#: comment/static/js/comment.js:506
+msgid "Flagging couldn't be processed!, please try again"
+msgstr"über"
+
+#: comment/static/js/comment.js:551
+msgid "read more ..."
+msgstr"über"
+
+#: comment/static/js/comment.js:553
+msgid "read less"
+msgstr"über"
+
+#: comment/static/js/comment.js:581
+msgid "Flag rejected"
+msgstr"über"
+
+#: comment/static/js/comment.js:582 comment/static/js/comment.js:600
+msgid "Reject the flag"
+msgstr"über"
+
+#: comment/static/js/comment.js:587 comment/static/js/comment.js:595
+msgid "Resolve the flag"
+msgstr"über"
+
+#: comment/static/js/comment.js:594
+msgid "Flag resolved"
+msgstr"über"
diff --git a/comment/managers/followers.py b/comment/managers/followers.py
new file mode 100644
index 0000000..688b56f
--- /dev/null
+++ b/comment/managers/followers.py
@@ -0,0 +1,48 @@
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+from comment.utils import get_username_for_comment
+
+
+class FollowerManager(models.Manager):
+ def is_following(self, email, model_object):
+ content_type = ContentType.objects.get_for_model(model_object)
+ return self.filter(email=email, object_id=model_object.id, content_type=content_type).exists()
+
+ def follow(self, email, username, model_object):
+ if not email or self.is_following(email, model_object):
+ return None
+ return self.create(email=email, username=username, content_object=model_object)
+
+ def unfollow(self, email, model_object):
+ content_type = ContentType.objects.get_for_model(model_object)
+ self.get(email=email, object_id=model_object.id, content_type=content_type).delete()
+
+ def toggle_follow(self, email, username, model_object):
+ if not email:
+ return False
+ if self.is_following(email, model_object):
+ self.unfollow(email, model_object)
+ return False
+ self.follow(email, username, model_object)
+ return True
+
+ def follow_parent_thread_for_comment(self, comment):
+ """This method is used to set the comment's creator as a follower of own comment and the parent thread"""
+ if not comment.email:
+ return
+ username = get_username_for_comment(comment)
+ model_object = comment
+ if not comment.is_parent:
+ model_object = comment.parent
+ else:
+ # follow the main thread for parent comment
+ self.follow(comment.email, username, comment.content_object)
+ self.follow(comment.email, username, model_object)
+
+ def filter_for_model_object(self, model_obj):
+ content_type = ContentType.objects.get_for_model(model_obj)
+ return self.filter(content_type=content_type, object_id=model_obj.id)
+
+ def get_emails_for_model_object(self, model_obj):
+ return self.filter_for_model_object(model_obj).values_list('email', flat=True)
diff --git a/comment/messages.py b/comment/messages.py
index 9e593f0..88b6101 100644
--- a/comment/messages.py
+++ b/comment/messages.py
@@ -56,11 +56,12 @@ class ReactionInfo:
class EmailInfo:
- SUBJECT = _('Comment Confirmation Request')
+ CONFIRMATION_SUBJECT = _('Comment Confirmation Request')
CONFIRMATION_SENT = _('We have sent a verification link to your email.'
'The comment will be displayed after it is verified.')
INPUT_PLACEHOLDER = _('email address, this will be used for verification.')
INPUT_TITLE = _('email address, it will be used for verification.')
+ NOTIFICATION_SUBJECT = _('{username} added comment to "{thread_name}"')
LABEL = _('email')
@@ -69,3 +70,9 @@ class FlagState:
FLAGGED = _('Flagged')
REJECTED = _('Flag rejected by the moderator')
RESOLVED = _('Comment modified by the author')
+
+
+class FollowError:
+ EMAIL_INVALID = _('Please insert a valid email')
+ EMAIL_REQUIRED = _('Email is required to subscribe {model_object}')
+ SYSTEM_NOT_ENABLED = _('Subscribe system must be enabled')
diff --git a/comment/migrations/0011_follower.py b/comment/migrations/0011_follower.py
new file mode 100644
index 0000000..1f74be2
--- /dev/null
+++ b/comment/migrations/0011_follower.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.4 on 2020-12-21 12:26
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('comment', '0010_auto_20201023_1442'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Follower',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('email', models.EmailField(max_length=254)),
+ ('username', models.CharField(max_length=50)),
+ ('object_id', models.PositiveIntegerField()),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
+ ],
+ ),
+ ]
diff --git a/comment/mixins.py b/comment/mixins.py
index e4ec688..baafdee 100644
--- a/comment/mixins.py
+++ b/comment/mixins.py
@@ -7,7 +7,7 @@
from comment.conf import settings
from comment.utils import is_comment_admin, is_comment_moderator
from comment.validators import ValidatorMixin
-from comment.messages import ErrorMessage, FlagError
+from comment.messages import ErrorMessage, FlagError, FollowError
class AJAXRequiredMixin:
@@ -72,7 +72,7 @@ def dispatch(self, request, *args, **kwargs):
class BaseFlagMixin(ObjectLevelMixin, abc.ABC):
def dispatch(self, request, *args, **kwargs):
- if not getattr(settings, 'COMMENT_FLAGS_ALLOWED', 0):
+ if not getattr(settings, 'COMMENT_FLAGS_ALLOWED', False):
return HttpResponseForbidden(FlagError.SYSTEM_NOT_ENABLED)
return super().dispatch(request, *args, **kwargs)
@@ -100,3 +100,10 @@ def dispatch(self, request, *args, **kwargs):
if not self.has_permission(request):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
+
+
+class CanSubscribeMixin(BaseCommentMixin):
+ def dispatch(self, request, *args, **kwargs):
+ if not getattr(settings, 'COMMENT_ALLOW_SUBSCRIPTION', False):
+ return HttpResponseForbidden(FollowError.SYSTEM_NOT_ENABLED)
+ return super().dispatch(request, *args, **kwargs)
diff --git a/comment/models/__init__.py b/comment/models/__init__.py
index c3ce03c..00fa110 100644
--- a/comment/models/__init__.py
+++ b/comment/models/__init__.py
@@ -2,3 +2,4 @@
from comment.models.comments import *
from comment.models.reactions import *
from comment.models.flags import *
+from comment.models.followers import *
diff --git a/comment/models/comments.py b/comment/models/comments.py
index cf7c5c7..417a29c 100644
--- a/comment/models/comments.py
+++ b/comment/models/comments.py
@@ -8,7 +8,7 @@
from comment.managers import CommentManager
from comment.conf import settings
-from comment.utils import is_comment_moderator
+from comment.utils import is_comment_moderator, get_username_for_comment
class Comment(models.Model):
@@ -33,10 +33,11 @@ class Meta:
ordering = ['-posted', ]
def __str__(self):
+ username = get_username_for_comment(self)
if not self.parent:
- return f'comment by {self.user}: {self.content[:20]}'
+ return f'comment by {username}: {self.content[:20]}'
else:
- return f'reply by {self.user}: {self.content[:20]}'
+ return f'reply by {username}: {self.content[:20]}'
def __repr__(self):
return self.__str__()
diff --git a/comment/models/followers.py b/comment/models/followers.py
new file mode 100644
index 0000000..10080c8
--- /dev/null
+++ b/comment/models/followers.py
@@ -0,0 +1,21 @@
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.db import models
+
+from comment.managers.followers import FollowerManager
+
+
+class Follower(models.Model):
+ email = models.EmailField()
+ username = models.CharField(max_length=50)
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+ object_id = models.PositiveIntegerField()
+ content_object = GenericForeignKey()
+
+ objects = FollowerManager()
+
+ def __str__(self):
+ return f'{str(self.content_object)} followed by {self.email}'
+
+ def __repr__(self):
+ return self.__str__()
diff --git a/comment/service/__init__.py b/comment/service/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/comment/service/email.py b/comment/service/email.py
new file mode 100644
index 0000000..6446202
--- /dev/null
+++ b/comment/service/email.py
@@ -0,0 +1,98 @@
+from threading import Thread
+
+from django.contrib.sites.shortcuts import get_current_site
+from django.core import signing
+from django.core.mail import EmailMultiAlternatives, get_connection
+from django.template import loader
+from django.urls import reverse
+
+from comment.conf import settings
+from comment.messages import EmailInfo
+from comment.models import Follower
+from comment.utils import get_username_for_comment
+
+
+class DABEmailService(object):
+ def __init__(self, comment, request):
+ self.comment = comment
+ self.request = request
+ self.sender = settings.COMMENT_FROM_EMAIL
+ self.is_html = settings.COMMENT_SEND_HTML_EMAIL
+ self.thread = None
+
+ def get_msg_context(self, **context):
+ context['comment'] = self.comment
+ context['site'] = get_current_site(self.request)
+ context['contact'] = settings.COMMENT_CONTACT_EMAIL
+ return context
+
+ def get_message(self, subject, body, receivers, html_msg=None):
+ msg = EmailMultiAlternatives(subject, body, self.sender, receivers)
+ if html_msg:
+ msg.attach_alternative(html_msg, 'text/html')
+ return msg
+
+ def send_messages(self, messages):
+ connection = get_connection() # Use default email connection
+ self.thread = Thread(target=connection.send_messages, args=(messages,))
+ self.thread.start()
+
+ def get_message_templates(self, text_template, html_template, msg_context):
+ text_msg_template = loader.get_template(text_template)
+ text_msg = text_msg_template.render(msg_context)
+ html_msg = None
+ if self.is_html:
+ html_msg_template = loader.get_template(html_template)
+ html_msg = html_msg_template.render(msg_context)
+ return text_msg, html_msg
+
+ def send_confirmation_request(self, api=False):
+ comment_dict = self.comment.to_dict()
+ receivers = [comment_dict['email']]
+ key = signing.dumps(comment_dict, compress=True)
+ text_template = 'comment/anonymous/confirmation_request.txt'
+ html_template = 'comment/anonymous/confirmation_request.html'
+ subject = EmailInfo.CONFIRMATION_SUBJECT
+ if api:
+ confirmation_url = f'/api/comments/confirm/{key}/'
+ else:
+ confirmation_url = reverse('comment:confirm-comment', args=[key])
+
+ context = self.get_msg_context(confirmation_url=confirmation_url)
+ text_msg, html_msg = self.get_message_templates(text_template, html_template, context)
+ msg = self.get_message(subject, text_msg, receivers, html_msg=html_msg)
+ self.send_messages([msg])
+
+ def get_thread(self):
+ if self.comment.is_parent:
+ return self.comment.content_object
+ return self.comment.parent
+
+ def get_thread_name(self):
+ if self.comment.is_parent:
+ return str(self.comment.content_object)
+ return str(self.comment.parent).split(':')[0]
+
+ def get_subject_for_notification(self, thread_name):
+ username = get_username_for_comment(self.comment)
+ return EmailInfo.NOTIFICATION_SUBJECT.format(username=username, thread_name=thread_name)
+
+ def get_messages_for_notification(self, thread_name, receivers):
+ text_template = 'comment/notifications/notification.txt'
+ html_template = 'comment/notifications/notification.html'
+ subject = self.get_subject_for_notification(thread_name)
+ messages = []
+ for receiver in receivers:
+ context = self.get_msg_context(thread_name=thread_name, receiver=receiver.username)
+ text_msg, html_msg = self.get_message_templates(text_template, html_template, context)
+ messages.append(self.get_message(subject, text_msg, [receiver.email], html_msg=html_msg))
+ return messages
+
+ def send_notification_to_followers(self):
+ thread = self.get_thread()
+ followers = Follower.objects.filter_for_model_object(thread).exclude(email=self.comment.email)
+ if not followers:
+ return
+ thread_name = self.get_thread_name()
+ messages = self.get_messages_for_notification(thread_name, followers)
+ self.send_messages(messages)
diff --git a/comment/signals/post_save.py b/comment/signals/post_save.py
index 547b863..3a0350e 100644
--- a/comment/signals/post_save.py
+++ b/comment/signals/post_save.py
@@ -1,19 +1,17 @@
from django.dispatch import receiver
from django.db.models import signals
-from comment.models import Comment, Flag, FlagInstance, Reaction, ReactionInstance
+from comment.models import Comment, Flag, FlagInstance, Reaction, ReactionInstance, Follower
+from comment.conf import settings
@receiver(signals.post_save, sender=Comment)
-def add_reaction(sender, instance, created, raw, using, update_fields, **kwargs):
+def add_initial_instances(sender, instance, created, raw, using, update_fields, **kwargs):
if created:
Reaction.objects.create(comment=instance)
-
-
-@receiver(signals.post_save, sender=Comment)
-def add_flag(sender, instance, created, raw, using, update_fields, **kwargs):
- if created:
Flag.objects.create(comment=instance)
+ if settings.COMMENT_ALLOW_SUBSCRIPTION:
+ Follower.objects.follow_parent_thread_for_comment(comment=instance)
@receiver(signals.post_save, sender=FlagInstance)
diff --git a/comment/static/css/comment.css b/comment/static/css/comment.css
index 4c4729b..72b0a42 100644
--- a/comment/static/css/comment.css
+++ b/comment/static/css/comment.css
@@ -70,12 +70,15 @@ input#search-input:focus::placeholder {
#comments .user-has-flagged {
fill: #ffc96c;
}
+
#comments .user-has-not-reacted,
-#comments .user-has-not-flagged {
+#comments .user-has-not-flagged,
+#comments .user-has-followed {
fill: none;
}
#comments .user-has-flagged,
-#comments .user-has-not-flagged {
+#comments .user-has-not-flagged,
+#comments .comment-follow-icon {
cursor: pointer;
}
.flag-modal textarea {
@@ -85,7 +88,8 @@ input#search-input:focus::placeholder {
color: #999;
}
.flag-rejected,
-.flag-resolved {
+.flag-resolved,
+#comments .user-has-followed {
stroke: #00bc8c;
}
.comment-modal {
@@ -150,4 +154,37 @@ input#search-input:focus::placeholder {
color: #78281f;
background-color: #fadbd8;
border-color: #f8cdc8;
-}
\ No newline at end of file
+}
+
+.three-dots {
+ position: relative;
+ width: 5px;
+ height: 5px;
+ background-color: #666;
+ border-radius: 50%;
+ margin-bottom: 2px;
+ margin-top: 2px;
+}
+.three-dots-wrapper {
+ cursor: pointer;
+ width: 10px;
+ padding: 3px;
+}
+
+.three-dots-dropdown {
+ position: absolute;
+ background-color: #f9f9f9;
+ overflow: auto;
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
+ z-index: 10;
+ padding: 0;
+ margin: 0;
+ right: 0;
+}
+.three-dots-dropdown > li {
+ border-bottom: 1px solid #e5e5e5;
+ padding: 0 10px;
+}
+.three-dots-dropdown > li:hover {
+ background: #f1f1f1;
+}
diff --git a/comment/static/js/comment.js b/comment/static/js/comment.js
index c3fe44f..fe039f9 100644
--- a/comment/static/js/comment.js
+++ b/comment/static/js/comment.js
@@ -5,21 +5,28 @@ document.addEventListener('DOMContentLoaded', () => {
'use strict';
let currentDeleteCommentButton, commentBeforeEdit;
let csrfToken = window.CSRF_TOKEN;
+ let deleteModal = document.getElementById("Modal");
+ let flagModal = document.getElementById('flagModal');
+ let followModal = document.getElementById('followModal');
let headers = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken,
'Content-Type': 'application/x-www-form-urlencoded'
};
document.getElementsByClassName(".js-comment-input").value = '';
- let removeTargetElement = e => {
+ let removeTargetElement = () => {
let currentHeight = window.pageYOffset;
window.location.replace("#");
// slice off the remaining '#':
if (typeof window.history.replaceState == 'function') {
history.replaceState({}, '', window.location.href.slice(0, -1));
}
- window.scrollTo(0, currentHeight);
- }
+ window.scrollTo(0, currentHeight);
+ // close three-dots-menus
+ Array.prototype.forEach.call(document.getElementsByClassName('js-three-dots-menu'), element => {
+ element.classList.add('d-none');
+ });
+ };
let showModal = modalElement => {
modalElement.style.display = 'block';
@@ -107,11 +114,13 @@ document.addEventListener('DOMContentLoaded', () => {
let submitCommentCreateForm = form => {
let formButton = form.querySelector("button");
let url = form.getAttribute('data-url') || window.location.href;
+ const urlParams = new window.URLSearchParams(window.location.search);
let formData = serializeObject(form);
+ formData.page = urlParams.get('page');
// this step is needed to append the form data to request.POST
let formDataQuery = convertFormDataToURLQuery(formData);
fetch(url, {
- method: 'POST',
+ method: 'POST',
headers: headers,
body: formDataQuery
}).then(response => {
@@ -124,7 +133,6 @@ document.addEventListener('DOMContentLoaded', () => {
return Promise.reject(response);
}).then(data => {
if (data.type === 'error') {
- console.log(data);
alert(data.detail);
return;
}
@@ -147,6 +155,10 @@ document.addEventListener('DOMContentLoaded', () => {
reply.textContent = gettext("Reply");
}
commentCount(1);
+ // update followBtn
+ let followButton = form.parentElement.previousElementSibling.querySelector(".js-comment-follow");
+ followButton.querySelector('.comment-follow-icon').classList.add('user-has-followed');
+ followButton.querySelector('span').setAttribute('title', 'Unfollow this thread');
}
formButton.setAttribute('disabled', 'disabled');
let elements = document.getElementsByClassName("js-comment-input");
@@ -160,7 +172,7 @@ document.addEventListener('DOMContentLoaded', () => {
let clean_uri = uri.substring(0, uri.indexOf("?"));
window.history.replaceState({}, document.title, clean_uri);
}
- }).catch(error => {
+ }).catch(() => {
alert(gettext("Unable to post your comment!, please try again"));
});
};
@@ -181,7 +193,7 @@ document.addEventListener('DOMContentLoaded', () => {
textAreaElement.value = '';
textAreaElement.value = value;
textAreaElement.setAttribute("style", "height: " + textAreaElement.scrollHeight + "px;");
- }).catch(error => {
+ }).catch(() => {
alert(gettext("You can't edit this comment"));
});
};
@@ -204,7 +216,7 @@ document.addEventListener('DOMContentLoaded', () => {
}).then(data => {
let updatedContentElement = stringToDom(data, '.js-updated-comment');
form.parentElement.replaceWith(updatedContentElement);
- }).catch(error => {
+ }).catch(() => {
alert(gettext("Modification didn't take effect!, please try again"));
});
};
@@ -245,9 +257,8 @@ document.addEventListener('DOMContentLoaded', () => {
fetch(url, {headers: headers}).then(response => {
return response.json()
}).then(data => {
- let modal = document.getElementById("Modal");
- showModal(modal);
- let modalContent = modal.querySelector('.comment-modal-content');
+ showModal(deleteModal);
+ let modalContent = deleteModal.querySelector('.comment-modal-content');
modalContent.innerHTML = data.html_form;
}).catch(error => {
console.error(error);
@@ -290,11 +301,10 @@ document.addEventListener('DOMContentLoaded', () => {
// update total count of comments
commentCount(-1);
}
- let modal = document.getElementById("Modal");
- hideModal(modal);
+ hideModal(deleteModal);
commentElement.remove();
- }).catch(error => {
+ }).catch(() => {
alert(gettext("Unable to delete your comment!, please try again"));
});
};
@@ -351,11 +361,53 @@ document.addEventListener('DOMContentLoaded', () => {
fillReaction(parentReactionEle, targetReaction);
changeReactionCount(parentReactionEle, data.likes, data.dislikes);
}
- }).catch(error => {
+ }).catch(() => {
alert(gettext("Reaction couldn't be processed!, please try again"));
});
};
+ let toggleFollow = (followButton, form) => {
+ let formDataQuery = null;
+ if (form) {
+ let formData = serializeObject(form);
+ formDataQuery = convertFormDataToURLQuery(formData);
+ }
+ let url = followButton.getAttribute('data-url');
+ fetch(url, {
+ method: 'POST',
+ headers: headers,
+ body: formDataQuery,
+ }).then(response => {
+ return response.json();
+ }).then(data => {
+ const infoElement = data.app_name === 'comment'
+ ? followButton.closest('.js-parent-comment')
+ : document.getElementById('comments').querySelector('.js-comment');
+ if (data.email_required) {
+ followModal.querySelector('form').setAttribute('data-target-btn-id', followButton.getAttribute('id'));
+ showModal(followModal);
+ }
+ else if (data.invalid_email) {
+ form.querySelector('.error').innerHTML = data.invalid_email;
+ }
+ else if (data.following) {
+ followButton.querySelector('.comment-follow-icon').classList.add('user-has-followed');
+ followButton.querySelector('span').setAttribute('title', 'Unfollow this thread');
+ const msg = gettext(`You are now subscribing "${data.model_object}"`);
+ createInfoElement(infoElement, 'success', msg);
+ hideModal(followModal);
+ } else {
+ followButton.querySelector('.comment-follow-icon').classList.remove('user-has-followed');
+ followButton.querySelector('span').setAttribute('title', 'Follow this thread');
+ const msg = gettext(`"${data.model_object}" is now unsubscribed`);
+ createInfoElement(infoElement, 'success', msg);
+ hideModal(followModal);
+ }
+ }).catch(() => {
+ alert(gettext("Subscription couldn't be processed!, please try again"));
+ });
+ };
+
let fadeOut = (element, duration) => {
let interval = 10;//ms
let opacity = 1.0;
@@ -453,21 +505,19 @@ document.addEventListener('DOMContentLoaded', () => {
flagIcon.parentElement.title = 'Report Comment';
toggleClass(flagIcon, addClass, removeClass, 'remove');
}
- let modal = document.getElementById('flagModal');
- hideModal(modal);
+ hideModal(flagModal);
if (data) {
createInfoElement(flagButton.closest('.js-parent-comment'), data.status, data.msg);
}
- }).catch(error => {
+ }).catch(() => {
alert(gettext("Flagging couldn't be processed!, please try again"));
});
};
let handleFlagModal = flagButton => {
- let modal = document.getElementById('flagModal');
- showModal(modal);
- document.getElementById('flagModal').querySelector('textarea').value = '';
- let form = modal.querySelector('.flag-modal-form');
+ showModal(flagModal);
+ flagModal.querySelector('textarea').value = '';
+ let form = flagModal.querySelector('.flag-modal-form');
let lastReason = form.querySelector('.flag-last-reason');
let flagInfo = form.querySelector('textarea');
flagInfo.style.display = "none";
@@ -484,7 +534,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
};
- let submit = modal.querySelector('.flag-modal-submit');
+ let submit = flagModal.querySelector('.flag-modal-submit');
submit.onclick = e => {
e.preventDefault();
let choice = form.querySelector('input[name="reason"]:checked');
@@ -563,15 +613,18 @@ document.addEventListener('DOMContentLoaded', () => {
});
};
+ let openThreeDostMenu = threeDotBtn => {
+ threeDotBtn.nextElementSibling.classList.toggle('d-none');
+ };
+
document.addEventListener('click', (event) => {
- removeTargetElement(event);
+ removeTargetElement();
if (event.target && event.target !== event.currentTarget) {
- let modal = document.getElementById("Modal");
- let flagModal = document.getElementById('flagModal');
- if (event.target === modal || event.target === flagModal ||
+ if (event.target === deleteModal || event.target === flagModal || event.target === followModal ||
event.target.closest('.modal-close-btn') || event.target.closest('.modal-cancel-btn')) {
- hideModal(modal);
- hideModal(flagModal)
+ hideModal(deleteModal);
+ hideModal(flagModal);
+ hideModal(followModal);
}
else if(event.target.closest('.js-reply-link')) {
event.preventDefault();
@@ -605,6 +658,14 @@ document.addEventListener('DOMContentLoaded', () => {
event.preventDefault();
toggleFlagState(event.target.closest('.js-flag-reject') || event.target.closest('.js-flag-resolve'));
}
+ else if (event.target.closest('.js-comment-follow')) {
+ event.preventDefault();
+ toggleFollow(event.target.closest('.js-comment-follow'));
+ }
+ else if (event.target.closest('.js-three-dots')) {
+ event.preventDefault();
+ openThreeDostMenu(event.target.closest('.js-three-dots'));
+ }
}
}, false);
@@ -624,6 +685,11 @@ document.addEventListener('DOMContentLoaded', () => {
event.preventDefault();
submitDeleteCommentForm(event.target);
}
+ else if (event.target.classList.contains('js-comment-follow-form')) {
+ event.preventDefault();
+ let followButton = document.getElementById(event.target.getAttribute('data-target-btn-id'));
+ toggleFollow(followButton, event.target);
+ }
}
}, false);
diff --git a/comment/templates/comment/anonymous/confirmation_request.html b/comment/templates/comment/anonymous/confirmation_request.html
index 9659949..0163ac2 100644
--- a/comment/templates/comment/anonymous/confirmation_request.html
+++ b/comment/templates/comment/anonymous/confirmation_request.html
@@ -1,20 +1,46 @@
-{% comment %}Inspired from Daniel Rus Morales's django-comments-xtd: * https://github.com/danirus/django-comments-xtd{% endcomment %} {% load i18n %}
-
+{% extends 'comment/email/base.html' %}
+{% comment %}Inspired from Daniel Rus Morales's django-comments-xtd: * https://github.com/danirus/django-comments-xtd{% endcomment %}
+{% comment %}Email template is used from: * https://github.com/leemunroe/responsive-html-email-template{% endcomment %}
+{% load i18n %}
+{% block content %}
+
-
+
-
+ {{ comment.content }}
+
-
+
+
+
+
http://{{ site.domain }}{{ confirmation_url|slice:":40" }}...
-
-
-
- {% trans "Kind regards," %}
{{ site }}
+
+
+ ------
+
+{% endblock content %}
+{% block footer %}
+{% endblock footer %}
\ No newline at end of file
diff --git a/comment/templates/comment/anonymous/confirmation_request.txt b/comment/templates/comment/anonymous/confirmation_request.txt
index 66c1b5f..a91e66a 100644
--- a/comment/templates/comment/anonymous/confirmation_request.txt
+++ b/comment/templates/comment/anonymous/confirmation_request.txt
@@ -1,5 +1,5 @@
{% load i18n %}
-{% trans "Hey," %}
+{% trans "Hey there," %}
{% trans "You or someone on behalf of you have requested to post a comment at" %} {{ comment.posted }}, {% trans "to the following URL" %}.
diff --git a/comment/templates/comment/base.html b/comment/templates/comment/base.html
index d47e0bb..72bdc27 100644
--- a/comment/templates/comment/base.html
+++ b/comment/templates/comment/base.html
@@ -1,2 +1,15 @@
{% include 'comment/static.html' %}
{% include 'comment/comments/base.html' %}
+
+
+{% if allowed_flags %}
+ {% include "comment/flags/flag_modal.html" %}
+{% endif %}
+{% if is_subscription_allowed %}
+ {% include 'comment/follow/follow_modal.html' %}
+{% endif %}
\ No newline at end of file
diff --git a/comment/templates/comment/comments/base.html b/comment/templates/comment/comments/base.html
index d56749f..7a0059a 100644
--- a/comment/templates/comment/comments/base.html
+++ b/comment/templates/comment/comments/base.html
@@ -3,6 +3,9 @@