Skip to content

Commit

Permalink
feat(#43): send email notifications
Browse files Browse the repository at this point in the history
- Add Follower model.
- Parent comment and main thread can now be subscribed.
- Follow and unfollow functionality is integrated in the UI and the API
- Email notification is sent to all thread followers when creating a
  comment in it.
- Tests clean up.
- Adjust translation tests.
- update docs for subscription
  • Loading branch information
Radi85 committed Dec 28, 2020
1 parent c2fee17 commit 8774a93
Show file tree
Hide file tree
Showing 77 changed files with 3,774 additions and 1,368 deletions.
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
============

Expand Down
18 changes: 7 additions & 11 deletions comment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 8 additions & 1 deletion comment/admin.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
15 changes: 15 additions & 0 deletions comment/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
11 changes: 8 additions & 3 deletions comment/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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']

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


Expand Down
2 changes: 2 additions & 0 deletions comment/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
name='comments-flag-state-change'
),
re_path(r'^comments/confirm/(?P<key>[^/]+)/$', 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)
53 changes: 43 additions & 10 deletions comment/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
})
2 changes: 2 additions & 0 deletions comment/conf/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@
COMMENT_USE_EMAIL_FIRST_PART_AS_USERNAME = False

COMMENT_ALLOW_TRANSLATION = False

COMMENT_ALLOW_SUBSCRIPTION = False
Loading

0 comments on commit 8774a93

Please sign in to comment.