Skip to content

Commit

Permalink
feat(#190): handle access permission
Browse files Browse the repository at this point in the history
This will use status code 403 for access deneid in comment mixins and handle it in
JS functions.
Implement blocking views and the API.
  • Loading branch information
Radi85 committed May 5, 2021
1 parent ec9017e commit 2210574
Show file tree
Hide file tree
Showing 40 changed files with 880 additions and 268 deletions.
32 changes: 29 additions & 3 deletions comment/api/permissions.py
@@ -1,7 +1,9 @@
from rest_framework import permissions

from comment.conf import settings
from comment.utils import is_comment_admin, is_comment_moderator
from comment.utils import is_comment_admin, is_comment_moderator, can_block_user, can_moderate_flagging
from comment.messages import BlockUserMSG
from comment.models import BlockedUser


class IsOwnerOrReadOnly(permissions.BasePermission):
Expand All @@ -22,6 +24,25 @@ def has_object_permission(self, request, view, obj):
return obj.user == request.user


class UserPermittedOrReadOnly(permissions.BasePermission):
message = BlockUserMSG.NOT_PERMITTED

def has_permission(self, request, view):
data = request.POST or getattr(request, 'data', {})
return bool(
request.method in permissions.SAFE_METHODS or
not BlockedUser.objects.is_user_blocked(request.user.id, data.get('email'))
)


class CanCreatePermission(permissions.BasePermission):
"""
This will check if creating comment is permitted
"""
def has_permission(self, request, view):
return request.user.is_authenticated or settings.COMMENT_ALLOW_ANONYMOUS


class FlagEnabledPermission(permissions.BasePermission):
"""
This will check if the COMMENT_FLAGS_ALLOWED is enabled
Expand All @@ -32,10 +53,10 @@ def has_permission(self, request, view):

class CanChangeFlaggedCommentState(permissions.BasePermission):
def has_permission(self, request, view):
return is_comment_admin(request.user) or is_comment_moderator(request.user)
return can_moderate_flagging(request.user)

def has_object_permission(self, request, view, obj):
return obj.is_flagged and (is_comment_admin(request.user) or is_comment_moderator(request.user))
return obj.is_flagged


class SubscriptionEnabled(permissions.BasePermission):
Expand All @@ -51,3 +72,8 @@ 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)


class CanBlockUsers(permissions.BasePermission):
def has_permission(self, request, view):
return can_block_user(request.user)
22 changes: 9 additions & 13 deletions comment/api/serializers.py
Expand Up @@ -10,7 +10,7 @@
from comment.models import Comment, Flag, Reaction
from comment.utils import get_user_for_request, get_profile_instance
from comment.messages import EmailError
from comment.mixins import CommentCreateMixin
from comment.views import CommentCreateMixin


def get_profile_model():
Expand Down Expand Up @@ -99,25 +99,21 @@ class Meta:
model = Comment
fields = ('id', 'user', 'email', 'content', 'parent', 'posted', 'edited', 'reply_count', 'replies', 'urlhash')

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']

super().__init__(*args, **kwargs)

@staticmethod
def validate_email(value):
if not value:
raise serializers.ValidationError(EmailError.EMAIL_MISSING, code='required')
return value.strip().lower()
def validate_email(email):
if not email:
raise serializers.ValidationError(
detail={'email': [EmailError.EMAIL_REQUIRED_FOR_ANONYMOUS]}, code='required'
)
return email.strip().lower()

def create(self, validated_data):
request = self.context['request']
user = get_user_for_request(request)
content = validated_data.get('content')
email = validated_data.get('email')
if not user:
self.validate_email(email)
time_posted = timezone.now()

temp_comment = Comment(
Expand Down
1 change: 1 addition & 0 deletions comment/api/urls.py
Expand Up @@ -19,6 +19,7 @@
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'),
path('comments/toggle-blocking/', views.ToggleBlockingAPI.as_view(), name='toggle-blocking'),
]

urlpatterns = format_suffix_patterns(urlpatterns)
31 changes: 16 additions & 15 deletions comment/api/views.py
Expand Up @@ -7,17 +7,18 @@
from comment.validators import ValidatorMixin, ContentTypeValidator
from comment.api.serializers import CommentSerializer, CommentCreateSerializer
from comment.api.permissions import (
IsOwnerOrReadOnly, FlagEnabledPermission, CanChangeFlaggedCommentState,
SubscriptionEnabled, CanGetSubscribers)
IsOwnerOrReadOnly, FlagEnabledPermission, CanChangeFlaggedCommentState, SubscriptionEnabled,
CanGetSubscribers, CanCreatePermission, UserPermittedOrReadOnly, CanBlockUsers
)
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.mixins import CommentCreateMixin
from comment.views import BaseToggleFollowView, CommentCreateMixin, BaseToggleBlockingView


class CommentCreate(ValidatorMixin, generics.CreateAPIView):
serializer_class = CommentCreateSerializer
permission_classes = (CanCreatePermission, UserPermittedOrReadOnly)
api = True

def get_serializer_context(self):
Expand All @@ -42,13 +43,13 @@ def get_queryset(self):
class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly, UserPermittedOrReadOnly)


class CommentDetailForReaction(generics.RetrieveAPIView):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly, UserPermittedOrReadOnly)

def get_serializer_context(self):
context = super().get_serializer_context()
Expand Down Expand Up @@ -76,7 +77,7 @@ def post(self, request, *args, **kwargs):
class CommentDetailForFlag(generics.RetrieveAPIView):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, FlagEnabledPermission)
permission_classes = (permissions.IsAuthenticatedOrReadOnly, FlagEnabledPermission, UserPermittedOrReadOnly)

def get_serializer_context(self):
context = super().get_serializer_context()
Expand All @@ -100,21 +101,16 @@ def post(self, request, *args, **kwargs):
class CommentDetailForFlagStateChange(generics.RetrieveAPIView):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (CanChangeFlaggedCommentState, )
permission_classes = (CanChangeFlaggedCommentState, UserPermittedOrReadOnly)

def get_serializer_context(self):
context = super().get_serializer_context()
context['flag_update'] = True
return context

def post(self, request, *args, **kwargs):
comment = get_object_or_404(Comment, id=kwargs.get('pk'))
comment = self.get_object()
flag = Flag.objects.get_for_comment(comment)
if not comment.is_flagged:
return Response(
{'detail': FlagError.REJECT_UNFLAGGED_COMMENT},
status=status.HTTP_400_BAD_REQUEST
)
state = request.data.get('state') or request.POST.get('state')
try:
state = flag.get_clean_state(state)
Expand Down Expand Up @@ -150,7 +146,7 @@ def get(self, request, *args, **kwargs):
class ToggleFollowAPI(BaseToggleFollowView, APIView):
api = True
response_class = Response
permission_classes = (SubscriptionEnabled, permissions.IsAuthenticated)
permission_classes = (SubscriptionEnabled, permissions.IsAuthenticated, UserPermittedOrReadOnly)

def post(self, request, *args, **kwargs):
self.validate(request)
Expand All @@ -169,3 +165,8 @@ def get(self, request, *args, **kwargs):
'model_id': self.model_obj.id,
'followers': Follower.objects.get_emails_for_model_object(self.model_obj)
})


class ToggleBlockingAPI(BaseToggleBlockingView, APIView):
permission_classes = (CanBlockUsers,)
response_class = Response
3 changes: 2 additions & 1 deletion comment/conf/defaults.py
Expand Up @@ -37,4 +37,5 @@
COMMENT_DEFAULT_PROFILE_PIC_LOC = '/static/img/default.png'

COMMENT_ALLOW_BLOCKING_USERS = False
COMMENT_ALLOW_MODERATOR_PERFORM_BLOCKING = False
COMMENT_ALLOW_MODERATOR_TO_BLOCK = False
COMMENT_RESPONSE_FOR_BLOCKED_USER = 'You cannot perform this action at the moment! Contact the admin for more details'
29 changes: 24 additions & 5 deletions comment/managers/blocker.py
@@ -1,15 +1,34 @@
from django.db import models

from comment.conf import settings


class BlockedUserManager(models.Manager):
def is_user_blocked(self, user_id=None, email=None):
if not settings.COMMENT_ALLOW_BLOCKING_USERS:
return False
if user_id:
return self.is_user_blocked_by_id(user_id)
elif email:
return self.is_user_blocked_by_email(email)
return False

def is_user_blocked_by_id(self, user_id):
try:
user_id = int(user_id)
return self.filter(user_id=user_id, blocked=True).exists()
return self.filter(user_id=int(user_id), blocked=True).exists()
except (ValueError, TypeError):
if not email:
return False
return self.filter(email=email, blocked=True).exists()
return False

def is_user_blocked_by_email(self, email):
if not email:
return False
return self.filter(email=email, blocked=True).exists()

def get_or_create_blocked_user_for_comment(self, comment):
user_id = comment.user.id if comment.user else None
if user_id:
return self.get_or_create_blocked_user_by_user_id(user_id)
return self.get_or_create_blocked_user_by_email(comment.email)

def get_or_create_blocked_user_by_user_id(self, user_id):
return self.get_or_create(user_id=user_id)
Expand Down
16 changes: 15 additions & 1 deletion comment/messages.py
@@ -1,8 +1,12 @@
from django.utils.translation import gettext_lazy as _

from comment.conf import settings


class ErrorMessage:
LOGIN_URL_MISSING = _('Comment App: LOGIN_URL is not in the settings')
LOGIN_REQUIRED = _('Comment App: You must be logged in to perform this action.')
NOT_AUTHORIZED = _('You do not have permission to perform this action.')
METHOD_NOT_IMPLEMENTED = _('Your {class_name} class has not defined a {method_name} method, which is required.')
NON_AJAX_REQUEST = _('Only AJAX request are allowed')
INVALID_ORDER_ARGUMENT = _((
Expand Down Expand Up @@ -52,7 +56,7 @@ class ReactionError:

class EmailError:
EMAIL_INVALID = _('Enter a valid email address.')
EMAIL_MISSING = _('Email is required for posting anonymous comments.')
EMAIL_REQUIRED_FOR_ANONYMOUS = _('Email is required for posting anonymous comments.')
BROKEN_VERIFICATION_LINK = _('The link seems to be broken.')
USED_VERIFICATION_LINK = _('The comment has already been verified.')

Expand Down Expand Up @@ -86,3 +90,13 @@ class FlagState:
class FollowError:
EMAIL_REQUIRED = _('Email is required to subscribe {model_object}')
SYSTEM_NOT_ENABLED = _('Subscribe system must be enabled')


class BlockState:
UNBLOCKED = _('Unblocked')
BLOCKED = _('Blocked')


class BlockUserMSG:
NOT_PERMITTED = _(settings.COMMENT_RESPONSE_FOR_BLOCKED_USER)
INVALID = _('Invalid input data')

0 comments on commit 2210574

Please sign in to comment.