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 %} -

{% trans "Hey," %}

+{% 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 %} +

{% trans "Hey there," %}

-

{% trans "You or someone on behalf of you have requested to post a comment into this page:" %}
- http://{{ site.domain }}{{ comment.content_object.get_absolute_url }} {% blocktrans with posted_time=comment.posted %}at {{ posted_time }}.{% endblocktrans %} -

+

{% trans "You or someone on your behalf have requested to post a comment into this page:" %}
+ http://{{ site.domain }}{{ comment.content_object.get_absolute_url }} + {% blocktrans with posted_time=comment.posted %}at {{ posted_time }}.{% endblocktrans %} +

-

{% trans "The comment:" %}
- {{ comment.content }} +

{% trans "The comment:" %}

+

{{ comment.content }}


+
-

{% trans "If you do not wish to post the comment, please ignore this message or report an incident to" %} {{ contact }}. {% trans "Otherwise click on the link below to confirm the comment." %}

+

{% trans "If you do not wish to post the comment, please ignore this message or report an incident to" %} {{ contact }}. {% trans "Otherwise click on the button below to confirm the comment." %}

+ + + + + + + +
+

{% trans "If clicking does not work, you can also copy the below link and paste the address into your browser's address window" %}.

http://{{ site.domain }}{{ confirmation_url|slice:":40" }}...

- -

{% trans "If clicking does not work, you can also copy and paste the address into your browser's address window" %}.

- -

{% trans "Thanks for your comment!" %}
-- -
-

{% trans "Kind regards," %}
{{ site }}

+
+

{% trans "Thanks for your comment!" %}

+ ------ +

{% 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 @@
{% get_comments_count model_object user %} {% trans "Comments" %} + {% if is_subscription_allowed %} + {% include 'comment/follow/follow.html' with model_object=model_object btnId=0 %} + {% endif %} {% block comment_messages %} {% include "comment/comments/messages.html" %} {% endblock comment_messages %} @@ -15,17 +18,6 @@ {% include 'comment/comments/parent_comment.html' with placeholder='reply to this comment...' %} {% endfor %} - - - {% if allowed_flags %} - {% include "comment/flags/flag_modal.html" %} - {% endif %} {% if comments.paginator.num_pages > 1 %} {% block pagination %} {% include 'comment/comments/pagination.html' with active_btn='bg-success' text_style='text-success' li_cls='page-item rounded mx-1' %} diff --git a/comment/templates/comment/comments/comment_body.html b/comment/templates/comment/comments/comment_body.html index 12e938c..f283cd5 100644 --- a/comment/templates/comment/comments/comment_body.html +++ b/comment/templates/comment/comments/comment_body.html @@ -9,9 +9,9 @@ {% endif %} {% include 'comment/comments/comment_content.html' with comment=comment %} - {% if allowed_flags and comment.user != user %} + {% if comment.user != user %}
- {% include "comment/flags/flags.html" %} + {% include 'comment/comments/three_dots_menu.html' %}
{% endif %} diff --git a/comment/templates/comment/comments/comment_content.html b/comment/templates/comment/comments/comment_content.html index f4730eb..a4d77c6 100644 --- a/comment/templates/comment/comments/comment_content.html +++ b/comment/templates/comment/comments/comment_content.html @@ -1,7 +1,6 @@ {% load comment_tags %} {% load i18n %} -
{% block comment_content %} {% render_content comment 30 %} @@ -39,6 +38,11 @@ {% blocktrans with plural_str=reply_count|pluralize:"y,ies" %} Repl{{ plural_str }} {% endblocktrans %} + {% if comment.user == user and is_subscription_allowed %} +
+ {% include 'comment/follow/follow.html' with model_object=comment btnId=comment.id %} +
+ {% endif %} {% endif %} {% include "comment/reactions/reactions.html" with comment=comment %} diff --git a/comment/templates/comment/comments/three_dots_menu.html b/comment/templates/comment/comments/three_dots_menu.html new file mode 100644 index 0000000..b007cab --- /dev/null +++ b/comment/templates/comment/comments/three_dots_menu.html @@ -0,0 +1,28 @@ +{% if allowed_flags and is_subscription_allowed and comment.is_parent %} +
+
+
+
+
+
    +
  • + {% include 'comment/follow/follow.html' with model_object=comment btnId=comment.id %} +
  • +
  • + {% if comment.user != user %} + {% include "comment/flags/flags.html" %} + {% endif %} +
  • +
+{% else %} + {% if allowed_flags and comment.user != user %} +
+ {% include "comment/flags/flags.html" %} +
+ {% endif %} + {% if comment.is_parent and is_subscription_allowed %} +
+ {% include 'comment/follow/follow.html' with model_object=comment btnId=comment.id %} +
+ {% endif %} +{% endif %} diff --git a/comment/templates/comment/email/base.html b/comment/templates/comment/email/base.html new file mode 100644 index 0000000..781c028 --- /dev/null +++ b/comment/templates/comment/email/base.html @@ -0,0 +1,372 @@ + + + + + + {% block emailTemplateTitle %}Simple Transactional Email{% endblock %} + + + + + + + + + + + + \ No newline at end of file diff --git a/comment/templates/comment/email/footer.html b/comment/templates/comment/email/footer.html new file mode 100644 index 0000000..5072b4f --- /dev/null +++ b/comment/templates/comment/email/footer.html @@ -0,0 +1,12 @@ + + + + + + + +
+ Company Inc, 3 Abbey Road, San Francisco CA 94102 +
Don't like these emails? Unsubscribe. +
Powered by HTMLemail. +
\ No newline at end of file diff --git a/comment/templates/comment/flags/flag_modal.html b/comment/templates/comment/flags/flag_modal.html index ecf17a3..5c5a71a 100644 --- a/comment/templates/comment/flags/flag_modal.html +++ b/comment/templates/comment/flags/flag_modal.html @@ -3,13 +3,13 @@
-