chore: added waffle for allowing only verified users to create post (#37051)
This commit is contained in:
committed by
GitHub
parent
e1a801a700
commit
01beca4ebb
@@ -35,7 +35,7 @@ from common.djangoapps.student.roles import (
|
|||||||
from lms.djangoapps.course_api.blocks.api import get_blocks
|
from lms.djangoapps.course_api.blocks.api import get_blocks
|
||||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
||||||
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
|
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE, ONLY_VERIFIED_USERS_CAN_POST
|
||||||
from lms.djangoapps.discussion.views import is_privileged_user
|
from lms.djangoapps.discussion.views import is_privileged_user
|
||||||
from openedx.core.djangoapps.discussions.models import (
|
from openedx.core.djangoapps.discussions.models import (
|
||||||
DiscussionsConfiguration,
|
DiscussionsConfiguration,
|
||||||
@@ -385,7 +385,7 @@ def get_course(request, course_key, check_tab=True):
|
|||||||
'site_key': settings.RECAPTCHA_SITE_KEY,
|
'site_key': settings.RECAPTCHA_SITE_KEY,
|
||||||
},
|
},
|
||||||
"is_email_verified": request.user.is_active,
|
"is_email_verified": request.user.is_active,
|
||||||
"only_verified_users_can_post": False,
|
"only_verified_users_can_post": ONLY_VERIFIED_USERS_CAN_POST.is_enabled(course_key),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from pytz import UTC
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient, APITestCase
|
from rest_framework.test import APIClient, APITestCase
|
||||||
|
|
||||||
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
|
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE, ONLY_VERIFIED_USERS_CAN_POST
|
||||||
from lms.djangoapps.discussion.rest_api.utils import get_usernames_from_search_string
|
from lms.djangoapps.discussion.rest_api.utils import get_usernames_from_search_string
|
||||||
from xmodule.modulestore import ModuleStoreEnum
|
from xmodule.modulestore import ModuleStoreEnum
|
||||||
from xmodule.modulestore.django import modulestore
|
from xmodule.modulestore.django import modulestore
|
||||||
@@ -1064,6 +1064,7 @@ class CourseTopicsViewV3Test(DiscussionAPIViewTestMixin, CommentsServiceMockMixi
|
|||||||
assert vertical_keys == expected_non_courseware_keys
|
assert vertical_keys == expected_non_courseware_keys
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
@httpretty.activate
|
@httpretty.activate
|
||||||
@disable_signal(api, 'thread_created')
|
@disable_signal(api, 'thread_created')
|
||||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||||
@@ -1139,6 +1140,41 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|||||||
response_data = json.loads(response.content.decode('utf-8'))
|
response_data = json.loads(response.content.decode('utf-8'))
|
||||||
assert response_data == expected_response_data
|
assert response_data == expected_response_data
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
(False, False, status.HTTP_200_OK),
|
||||||
|
(False, True, status.HTTP_400_BAD_REQUEST),
|
||||||
|
(True, False, status.HTTP_200_OK),
|
||||||
|
(True, True, status.HTTP_200_OK),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_creation_for_non_verified_user(self, email_verified, only_verified_user_can_post, response_status):
|
||||||
|
"""
|
||||||
|
Tests posts cannot be created if ONLY_VERIFIED_USERS_CAN_POST is enabled and user email is unverified.
|
||||||
|
"""
|
||||||
|
with override_waffle_flag(ONLY_VERIFIED_USERS_CAN_POST, only_verified_user_can_post):
|
||||||
|
self.user.is_active = email_verified
|
||||||
|
self.user.save()
|
||||||
|
self.register_get_user_response(self.user)
|
||||||
|
cs_thread = make_minimal_cs_thread({
|
||||||
|
"id": "test_thread",
|
||||||
|
"username": self.user.username,
|
||||||
|
"read": True,
|
||||||
|
})
|
||||||
|
self.register_post_thread_response(cs_thread)
|
||||||
|
request_data = {
|
||||||
|
"course_id": str(self.course.id),
|
||||||
|
"topic_id": "test_topic",
|
||||||
|
"type": "discussion",
|
||||||
|
"title": "Test Title",
|
||||||
|
"raw_body": "# Test \n This is a very long body but will not be truncated for the preview.",
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url,
|
||||||
|
json.dumps(request_data),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
assert response.status_code == response_status
|
||||||
|
|
||||||
|
|
||||||
@httpretty.activate
|
@httpretty.activate
|
||||||
@disable_signal(api, 'thread_deleted')
|
@disable_signal(api, 'thread_deleted')
|
||||||
@@ -2019,6 +2055,7 @@ class CommentViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
@httpretty.activate
|
@httpretty.activate
|
||||||
@disable_signal(api, 'comment_created')
|
@disable_signal(api, 'comment_created')
|
||||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||||
@@ -2134,6 +2171,69 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
(False, False, status.HTTP_200_OK),
|
||||||
|
(False, True, status.HTTP_400_BAD_REQUEST),
|
||||||
|
(True, False, status.HTTP_200_OK),
|
||||||
|
(True, True, status.HTTP_200_OK),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_creation_for_non_verified_user(self, email_verified, only_verified_user_can_post, response_status):
|
||||||
|
"""
|
||||||
|
Tests comments/replies cannot be created if ONLY_VERIFIED_USERS_CAN_POST is enabled and
|
||||||
|
user email is unverified.
|
||||||
|
"""
|
||||||
|
with override_waffle_flag(ONLY_VERIFIED_USERS_CAN_POST, only_verified_user_can_post):
|
||||||
|
self.user.is_active = email_verified
|
||||||
|
self.user.save()
|
||||||
|
self.register_get_user_response(self.user)
|
||||||
|
self.register_thread()
|
||||||
|
self.register_comment()
|
||||||
|
request_data = {
|
||||||
|
"thread_id": "test_thread",
|
||||||
|
"raw_body": "Test body",
|
||||||
|
}
|
||||||
|
expected_response_data = {
|
||||||
|
"id": "test_comment",
|
||||||
|
"thread_id": "test_thread",
|
||||||
|
"parent_id": None,
|
||||||
|
"author": self.user.username,
|
||||||
|
"author_label": None,
|
||||||
|
"created_at": "1970-01-01T00:00:00Z",
|
||||||
|
"updated_at": "1970-01-01T00:00:00Z",
|
||||||
|
"raw_body": "Test body",
|
||||||
|
"rendered_body": "<p>Test body</p>",
|
||||||
|
"endorsed": False,
|
||||||
|
"endorsed_by": None,
|
||||||
|
"endorsed_by_label": None,
|
||||||
|
"endorsed_at": None,
|
||||||
|
"abuse_flagged": False,
|
||||||
|
"abuse_flagged_any_user": None,
|
||||||
|
"voted": False,
|
||||||
|
"vote_count": 0,
|
||||||
|
"children": [],
|
||||||
|
"editable_fields": ["abuse_flagged", "anonymous", "raw_body"],
|
||||||
|
"child_count": 0,
|
||||||
|
"can_delete": True,
|
||||||
|
"anonymous": False,
|
||||||
|
"anonymous_to_peers": False,
|
||||||
|
"last_edit": None,
|
||||||
|
"edit_by_label": None,
|
||||||
|
"profile_image": {
|
||||||
|
"has_image": False,
|
||||||
|
"image_url_full": "http://testserver/static/default_500.png",
|
||||||
|
"image_url_large": "http://testserver/static/default_120.png",
|
||||||
|
"image_url_medium": "http://testserver/static/default_50.png",
|
||||||
|
"image_url_small": "http://testserver/static/default_30.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
self.url,
|
||||||
|
json.dumps(request_data),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
assert response.status_code == response_status
|
||||||
|
|
||||||
|
|
||||||
@httpretty.activate
|
@httpretty.activate
|
||||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from lms.djangoapps.course_api.blocks.api import get_blocks
|
|||||||
from lms.djangoapps.course_goals.models import UserActivity
|
from lms.djangoapps.course_goals.models import UserActivity
|
||||||
from lms.djangoapps.discussion.rest_api.permissions import IsAllowedToBulkDelete
|
from lms.djangoapps.discussion.rest_api.permissions import IsAllowedToBulkDelete
|
||||||
from lms.djangoapps.discussion.rest_api.tasks import delete_course_post_for_user
|
from lms.djangoapps.discussion.rest_api.tasks import delete_course_post_for_user
|
||||||
|
from lms.djangoapps.discussion.toggles import ONLY_VERIFIED_USERS_CAN_POST
|
||||||
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
|
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
|
||||||
from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service
|
from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service
|
||||||
from lms.djangoapps.instructor.access import update_forum_role
|
from lms.djangoapps.instructor.access import update_forum_role
|
||||||
@@ -671,13 +672,19 @@ class ThreadViewSet(DeveloperErrorViewMixin, ViewSet):
|
|||||||
"""
|
"""
|
||||||
if not request.data.get("course_id"):
|
if not request.data.get("course_id"):
|
||||||
raise ValidationError({"course_id": ["This field is required."]})
|
raise ValidationError({"course_id": ["This field is required."]})
|
||||||
if is_captcha_enabled(CourseKey.from_string(request.data.get("course_id"))):
|
course_key_str = request.data.get("course_id")
|
||||||
|
course_key = CourseKey.from_string(course_key_str)
|
||||||
|
if is_captcha_enabled(course_key):
|
||||||
captcha_token = request.data.get('captcha_token')
|
captcha_token = request.data.get('captcha_token')
|
||||||
if not captcha_token:
|
if not captcha_token:
|
||||||
raise ValidationError({'captcha_token': 'This field is required.'})
|
raise ValidationError({'captcha_token': 'This field is required.'})
|
||||||
|
|
||||||
if not verify_recaptcha_token(captcha_token):
|
if not verify_recaptcha_token(captcha_token):
|
||||||
return Response({'error': 'CAPTCHA verification failed.'}, status=400)
|
return Response({'error': 'CAPTCHA verification failed.'}, status=400)
|
||||||
|
|
||||||
|
if ONLY_VERIFIED_USERS_CAN_POST.is_enabled(course_key) and not request.user.is_active:
|
||||||
|
raise ValidationError({"detail": "Only verified users can post in discussions."})
|
||||||
|
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
data.pop('captcha_token', None)
|
data.pop('captcha_token', None)
|
||||||
return Response(create_thread(request, data))
|
return Response(create_thread(request, data))
|
||||||
@@ -1037,14 +1044,20 @@ class CommentViewSet(DeveloperErrorViewMixin, ViewSet):
|
|||||||
"""
|
"""
|
||||||
if not request.data.get("thread_id"):
|
if not request.data.get("thread_id"):
|
||||||
raise ValidationError({"thread_id": ["This field is required."]})
|
raise ValidationError({"thread_id": ["This field is required."]})
|
||||||
course_id = get_course_id_from_thread_id(request.data["thread_id"])
|
course_key_str = get_course_id_from_thread_id(request.data["thread_id"])
|
||||||
if is_captcha_enabled(CourseKey.from_string(course_id)):
|
course_key = CourseKey.from_string(course_key_str)
|
||||||
|
|
||||||
|
if is_captcha_enabled(course_key):
|
||||||
captcha_token = request.data.get('captcha_token')
|
captcha_token = request.data.get('captcha_token')
|
||||||
if not captcha_token:
|
if not captcha_token:
|
||||||
raise ValidationError({'captcha_token': 'This field is required.'})
|
raise ValidationError({'captcha_token': 'This field is required.'})
|
||||||
|
|
||||||
if not verify_recaptcha_token(captcha_token):
|
if not verify_recaptcha_token(captcha_token):
|
||||||
return Response({'error': 'CAPTCHA verification failed.'}, status=400)
|
return Response({'error': 'CAPTCHA verification failed.'}, status=400)
|
||||||
|
|
||||||
|
if ONLY_VERIFIED_USERS_CAN_POST.is_enabled(course_key) and not request.user.is_active:
|
||||||
|
raise ValidationError({"detail": "Only verified users can post in discussions."})
|
||||||
|
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
data.pop('captcha_token', None)
|
data.pop('captcha_token', None)
|
||||||
return Response(create_comment(request, data))
|
return Response(create_comment(request, data))
|
||||||
|
|||||||
@@ -15,3 +15,15 @@ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
|||||||
ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(
|
ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(
|
||||||
f"{WAFFLE_FLAG_NAMESPACE}.enable_discussions_mfe", __name__
|
f"{WAFFLE_FLAG_NAMESPACE}.enable_discussions_mfe", __name__
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# .. toggle_name: discussions.only_verified_users_can_post
|
||||||
|
# .. toggle_implementation: CourseWaffleFlag
|
||||||
|
# .. toggle_default: False
|
||||||
|
# .. toggle_description: Waffle flag to allow only verified users to post in discussions
|
||||||
|
# .. toggle_use_cases: temporary, open_edx
|
||||||
|
# .. toggle_creation_date: 2025-22-07
|
||||||
|
# .. toggle_target_removal_date: 2026-04-01
|
||||||
|
ONLY_VERIFIED_USERS_CAN_POST = CourseWaffleFlag(
|
||||||
|
f"{WAFFLE_FLAG_NAMESPACE}.only_verified_users_can_post", __name__
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user