Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements backend core to incorporate Flagging Mechanism #4537

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions contentcuration/contentcuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ def check_space(self, size, checksum):
if space < size:
raise PermissionDenied(_("Not enough space. Check your storage under Settings page."))

def check_feature_flag(self, flag_name):
feature_flags = self.feature_flags or {}
return feature_flags.get(flag_name, False)

def check_channel_space(self, channel):
active_files = self.get_user_active_files()
staging_tree_id = channel.staging_tree.tree_id
Expand Down
45 changes: 45 additions & 0 deletions contentcuration/contentcuration/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from contentcuration.models import Channel
from contentcuration.models import ContentNode
from contentcuration.models import DEFAULT_CONTENT_DEFAULTS
from contentcuration.tests import testdata
from contentcuration.viewsets.channel import ChannelSerializer as BaseChannelSerializer
from contentcuration.viewsets.common import ContentDefaultsSerializer
from contentcuration.viewsets.contentnode import ContentNodeSerializer
from contentcuration.viewsets.feedback import FlagFeedbackEventSerializer


def ensure_no_querysets_in_serializer(object):
Expand Down Expand Up @@ -178,3 +180,46 @@ def test_save__update(self):
self.assertEqual(
dict(author="Buster", license="Special Permissions"), c.content_defaults
)


class FlagFeedbackSerializerTestCase(BaseAPITestCase):
def setUp(self):
super(FlagFeedbackSerializerTestCase, self).setUp()
self.channel = testdata.channel("testchannel")
self.flagged_node = testdata.node(
{
"kind_id": content_kinds.VIDEO,
"title": "Suspicious Video content",
},
)

def _create_base_feedback_data(self, context, contentnode_id, content_id):
base_feedback_data = {
'context': context,
'contentnode_id': contentnode_id,
'content_id': content_id,
}
return base_feedback_data

def test_deserialization_and_validation(self):
data = {
'user': self.user.id,
'target_channel_id': str(self.channel.id),
'context': {'test_key': 'test_value'},
'contentnode_id': str(self.flagged_node.id),
'content_id': str(self.flagged_node.content_id),
'feedback_type': 'FLAGGED',
'feedback_reason': 'Reason1.....'
}
serializer = FlagFeedbackEventSerializer(data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
instance = serializer.save()
self.assertEqual(instance.context, data['context'])
self.assertEqual(instance.user.id, data['user'])
self.assertEqual(instance.feedback_type, data['feedback_type'])
self.assertEqual(instance.feedback_reason, data['feedback_reason'])

def test_invalid_data(self):
data = {'context': 'invalid'}
serializer = FlagFeedbackEventSerializer(data=data)
self.assertFalse(serializer.is_valid())
4 changes: 3 additions & 1 deletion contentcuration/contentcuration/tests/testdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,13 @@ def random_string(chars=10):
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(chars))


def user(email='[email protected]'):
def user(email='[email protected]', feature_flags=None):
user, is_new = cc.User.objects.get_or_create(email=email)
if is_new:
user.set_password('password')
user.is_active = True
if feature_flags is not None:
user.feature_flags = feature_flags
user.save()
return user

Expand Down
134 changes: 134 additions & 0 deletions contentcuration/contentcuration/tests/viewsets/test_flagged.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from django.urls import reverse
from le_utils.constants import content_kinds

from contentcuration.models import FlagFeedbackEvent
from contentcuration.tests import testdata
from contentcuration.tests.base import StudioAPITestCase


class CRUDTestCase(StudioAPITestCase):
@property
def flag_feedback_object(self):
return {
'context': {'spam': 'Spam or misleading'},
'contentnode_id': self.contentNode.id,
'content_id': self.contentNode.content_id,
'target_channel_id': self.channel.id,
'user': self.user.id,
'feedback_type': 'FLAGGED',
'feedback_reason': 'Some reason provided by the user'
}

def setUp(self):
super(CRUDTestCase, self).setUp()
self.contentNode = testdata.node(
{
"kind_id": content_kinds.VIDEO,
"title": "Suspicious Video content",
},
)
self.channel = testdata.channel()
self.user = testdata.user(feature_flags={"test_dev_feature": True})

def test_create_flag_event(self):
self.client.force_authenticate(user=self.user)
flagged_content = self.flag_feedback_object
response = self.client.post(
reverse("flagged-list"), flagged_content, format="json",
)
self.assertEqual(response.status_code, 201, response.content)

def test_create_flag_event_fails_for_flag_test_dev_feature_disabled(self):
flagged_content = self.flag_feedback_object
self.user.feature_flags = {'test_dev_feature': False}
self.user.save()
self.client.force_authenticate(user=self.user)
response = self.client.post(
reverse("flagged-list"), flagged_content, format="json",
)
self.assertEqual(response.status_code, 403, response.content)

def test_create_flag_event_fails_for_flag_test_dev_feature_None(self):
flagged_content = self.flag_feedback_object
self.user.feature_flags = None
self.user.save()
self.client.force_authenticate(user=self.user)
response = self.client.post(
reverse("flagged-list"), flagged_content, format="json",
)
self.assertEqual(response.status_code, 403, response.content)

def test_create_flag_event_fails_for_unauthorized_user(self):
flagged_content = self.flag_feedback_object
response = self.client.post(
reverse("flagged-list"), flagged_content, format="json",
)
self.assertEqual(response.status_code, 403, response.content)

def test_list_flagged_content_super_admin(self):
self.user.is_admin = True
self.user.save()
self.client.force_authenticate(self.user)
response = self.client.get(reverse("flagged-list"), format="json")
self.assertEqual(response.status_code, 200, response.content)

def test_retreive_fails_for_normal_user(self):
self.client.force_authenticate(user=self.user)
flag_feedback_object = FlagFeedbackEvent.objects.create(
**{
'context': {'spam': 'Spam or misleading'},
'contentnode_id': self.contentNode.id,
'content_id': self.contentNode.content_id,
'target_channel_id': self.channel.id,
'feedback_type': 'FLAGGED',
'feedback_reason': 'Some reason provided by the user'
},
user=self.user,
)
response = self.client.get(reverse("flagged-detail", kwargs={"pk": flag_feedback_object.id}), format="json")
self.assertEqual(response.status_code, 403, response.content)

def test_list_fails_for_normal_user(self):
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse("flagged-list"), format="json")
self.assertEqual(response.status_code, 403, response.content)

def test_list_fails_for_user_dev_feature_enabled(self):
response = self.client.get(reverse("flagged-list"), format="json")
self.assertEqual(response.status_code, 403, response.content)

def test_destroy_flagged_content_super_admin(self):
self.user.is_admin = True
self.user.save()
self.client.force_authenticate(self.user)
flag_feedback_object = FlagFeedbackEvent.objects.create(
**{
'context': {'spam': 'Spam or misleading'},
'contentnode_id': self.contentNode.id,
'content_id': self.contentNode.content_id,
'target_channel_id': self.channel.id,
'feedback_type': 'FLAGGED',
'feedback_reason': 'Some reason provided by the user'
},
user=self.user,
)
response = self.client.delete(reverse("flagged-detail", kwargs={"pk": flag_feedback_object.id}), format="json")
self.assertEqual(response.status_code, 204, response.content)

def test_destroy_flagged_content_fails_for_user_with_feature_flag_disabled(self):
self.user.feature_flags = {'test_dev_feature': False}
self.user.save()
self.client.force_authenticate(user=self.user)
flag_feedback_object = FlagFeedbackEvent.objects.create(
**{
'context': {'spam': 'Spam or misleading'},
'contentnode_id': self.contentNode.id,
'content_id': self.contentNode.content_id,
'target_channel_id': self.channel.id,
'feedback_type': 'FLAGGED',
'feedback_reason': 'Some reason provided by the user'
},
user=self.user,
)
response = self.client.delete(reverse("flagged-detail", kwargs={"pk": flag_feedback_object.id}), format="json")
self.assertEqual(response.status_code, 403, response.content)
2 changes: 2 additions & 0 deletions contentcuration/contentcuration/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from contentcuration.viewsets.channelset import ChannelSetViewSet
from contentcuration.viewsets.clipboard import ClipboardViewSet
from contentcuration.viewsets.contentnode import ContentNodeViewSet
from contentcuration.viewsets.feedback import FlagFeedbackEventViewSet
from contentcuration.viewsets.file import FileViewSet
from contentcuration.viewsets.invitation import InvitationViewSet
from contentcuration.viewsets.sync.endpoint import SyncView
Expand Down Expand Up @@ -67,6 +68,7 @@ def get_redirect_url(self, *args, **kwargs):
router.register(r'assessmentitem', AssessmentItemViewSet)
router.register(r'admin-users', AdminUserViewSet, basename='admin-users')
router.register(r'clipboard', ClipboardViewSet, basename='clipboard')
router.register(r'flagged', FlagFeedbackEventViewSet, basename='flagged')

urlpatterns = [
re_path(r'^api/', include(router.urls)),
Expand Down
48 changes: 48 additions & 0 deletions contentcuration/contentcuration/viewsets/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from rest_framework import permissions
from rest_framework import serializers
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from contentcuration.models import FlagFeedbackEvent


class IsAdminForListAndDestroy(permissions.BasePermission):
def has_permission(self, request, view):
# only allow list and destroy of flagged content to admins
if view.action in ['list', 'destroy', 'retrieve']:
try:
return request.user and request.user.is_admin
except AttributeError:
return False
if request.user.check_feature_flag('test_dev_feature'):
return True
return False


class BaseFeedbackSerializer(serializers.ModelSerializer):
class Meta:
fields = ['id', 'context', 'contentnode_id', 'content_id']
read_only_fields = ['id']


class BaseFeedbackEventSerializer(serializers.ModelSerializer):
class Meta:
fields = ['user', 'target_channel_id']
ozer550 marked this conversation as resolved.
Show resolved Hide resolved
read_only_fields = ['user']


class BaseFeedbackInteractionEventSerializer(serializers.ModelSerializer):
class Meta:
fields = ['feedback_type', 'feedback_reason']


class FlagFeedbackEventSerializer(BaseFeedbackSerializer, BaseFeedbackEventSerializer, BaseFeedbackInteractionEventSerializer):
class Meta:
model = FlagFeedbackEvent
fields = BaseFeedbackSerializer.Meta.fields + BaseFeedbackEventSerializer.Meta.fields + BaseFeedbackInteractionEventSerializer.Meta.fields


class FlagFeedbackEventViewSet(viewsets.ModelViewSet):
queryset = FlagFeedbackEvent.objects.all()
serializer_class = FlagFeedbackEventSerializer
permission_classes = [IsAuthenticated, IsAdminForListAndDestroy]