Skip to content

Commit

Permalink
Merge 2210574 into bdfdea1
Browse files Browse the repository at this point in the history
  • Loading branch information
Radi85 committed May 5, 2021
2 parents bdfdea1 + 2210574 commit 1d56ece
Show file tree
Hide file tree
Showing 50 changed files with 1,223 additions and 261 deletions.
18 changes: 17 additions & 1 deletion comment/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.contrib import admin

from comment.models import Comment, Flag, FlagInstance, Reaction, ReactionInstance, Follower
from comment.models import (
Comment, Flag, FlagInstance, Reaction, ReactionInstance, Follower, BlockedUser, BlockedUserHistory
)


class CommentModelAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -43,7 +45,21 @@ class FollowerModelAdmin(admin.ModelAdmin):
search_fields = ('email',)


class BlockedUserModelAdmin(admin.ModelAdmin):
list_display = ('user', 'email', 'blocked')
search_fields = ('user__username', 'email')


class BlockedUserHistoryModelAdmin(admin.ModelAdmin):
list_display = ('blocked_user', 'blocker', 'reason', 'state', 'date')
search_fields = (
'blocked_user__user__username', 'blocked_user__email', 'blocker__username', 'blocker__email', 'state', 'date'
)


admin.site.register(Comment, CommentModelAdmin)
admin.site.register(Reaction, ReactionModelAdmin)
admin.site.register(Flag, FlagModelAdmin)
admin.site.register(Follower, FollowerModelAdmin)
admin.site.register(BlockedUser, BlockedUserModelAdmin)
admin.site.register(BlockedUserHistory, BlockedUserHistoryModelAdmin)
32 changes: 29 additions & 3 deletions comment/api/permissions.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
4 changes: 4 additions & 0 deletions comment/conf/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@
COMMENT_WRAP_CONTENT_WORDS = 30

COMMENT_DEFAULT_PROFILE_PIC_LOC = '/static/img/default.png'

COMMENT_ALLOW_BLOCKING_USERS = 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'
1 change: 1 addition & 0 deletions comment/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ def __call__(self):
'is_anonymous_allowed': settings.COMMENT_ALLOW_ANONYMOUS,
'is_translation_allowed': settings.COMMENT_ALLOW_TRANSLATION,
'is_subscription_allowed': settings.COMMENT_ALLOW_SUBSCRIPTION,
'is_blocking_allowed': settings.COMMENT_ALLOW_BLOCKING_USERS,
'oauth': self.is_oauth()
}
1 change: 1 addition & 0 deletions comment/managers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from comment.managers.comments import *
from comment.managers.reactions import *
from comment.managers.flags import *
from comment.managers.blocker import *
40 changes: 40 additions & 0 deletions comment/managers/blocker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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:
return self.filter(user_id=int(user_id), blocked=True).exists()
except (ValueError, TypeError):
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)

def get_or_create_blocked_user_by_email(self, email):
try:
return self.get_or_create(email=email)
except self.model.MultipleObjectsReturned:
return self.filter(email=email, user=None).first(), False
16 changes: 15 additions & 1 deletion comment/messages.py
Original file line number Diff line number Diff line change
@@ -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')
36 changes: 36 additions & 0 deletions comment/migrations/0012_blockeduser_blockeduserhistory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 3.1.4 on 2021-04-03 10:57

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comment', '0011_follower'),
]

operations = [
migrations.CreateModel(
name='BlockedUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('blocked', models.BooleanField(default=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='BlockedUserHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reason', models.TextField(blank=True, null=True)),
('state', models.SmallIntegerField(choices=[(0, 'Unblocked'), (1, 'Blocked')], default=1)),
('date', models.DateTimeField(auto_now_add=True)),
('blocked_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='comment.blockeduser')),
('blocker', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
Loading

0 comments on commit 1d56ece

Please sign in to comment.