feat: reapply forum v2 changes (#36002)

* feat: Reapply "Integrate Forum V2 into edx-platform"

This reverts commit 818aa343a2.

* feat: make it possible to globally disable forum v2 with setting

We introduce a setting that allows us to bypass any course waffle flag
check. The advantage of such a setting is that we don't need to find the
course ID: in some cases, we might not have access to the course ID, and
we need to look for it... in forum v2.

See discussion here: https://github.com/openedx/forum/issues/137

* chore: bump openedx-forum to 0.1.5

This should fix an issue with index creation on edX.org.
This commit is contained in:
Régis Behmo
2024-12-12 08:18:33 +01:00
committed by GitHub
parent f96f92677f
commit 065adf398e
26 changed files with 1895 additions and 426 deletions

View File

@@ -82,6 +82,7 @@ class MockRequestSetupMixin:
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class CreateThreadGroupIdTestCase(
MockRequestSetupMixin,
CohortedTestCase,
@@ -90,7 +91,21 @@ class CreateThreadGroupIdTestCase(
):
cs_endpoint = "/threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
def call_view(self, mock_is_forum_v2_enabled, mock_request, commentable_id, user, group_id, pass_group_id=True):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {})
request_data = {"body": "body", "title": "title", "thread_type": "discussion"}
if pass_group_id:
@@ -105,8 +120,9 @@ class CreateThreadGroupIdTestCase(
commentable_id=commentable_id
)
def test_group_info_in_response(self, mock_request):
def test_group_info_in_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
@@ -116,6 +132,7 @@ class CreateThreadGroupIdTestCase(
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
@disable_signal(views, 'thread_edited')
@disable_signal(views, 'thread_voted')
@disable_signal(views, 'thread_deleted')
@@ -127,11 +144,18 @@ class ThreadActionGroupIdTestCase(
def call_view(
self,
view_name,
mock_is_forum_v2_enabled,
mock_request,
user=None,
post_params=None,
view_args=None
):
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(
mock_request,
{
@@ -154,53 +178,58 @@ class ThreadActionGroupIdTestCase(
**(view_args or {})
)
def test_update(self, mock_request):
def test_update(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
"update_thread",
mock_is_forum_v2_enabled,
mock_request,
post_params={"body": "body", "title": "title"}
)
self._assert_json_response_contains_group_info(response)
def test_delete(self, mock_request):
response = self.call_view("delete_thread", mock_request)
def test_delete(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view("delete_thread", mock_is_forum_v2_enabled, mock_request)
self._assert_json_response_contains_group_info(response)
def test_vote(self, mock_request):
def test_vote(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
"vote_for_thread",
mock_is_forum_v2_enabled,
mock_request,
view_args={"value": "up"}
)
self._assert_json_response_contains_group_info(response)
response = self.call_view("undo_vote_for_thread", mock_request)
response = self.call_view("undo_vote_for_thread", mock_is_forum_v2_enabled, mock_request)
self._assert_json_response_contains_group_info(response)
def test_flag(self, mock_request):
def test_flag(self, mock_is_forum_v2_enabled, mock_request):
with mock.patch('openedx.core.djangoapps.django_comment_common.signals.thread_flagged.send') as signal_mock:
response = self.call_view("flag_abuse_for_thread", mock_request)
response = self.call_view("flag_abuse_for_thread", mock_is_forum_v2_enabled, mock_request)
self._assert_json_response_contains_group_info(response)
self.assertEqual(signal_mock.call_count, 1)
response = self.call_view("un_flag_abuse_for_thread", mock_request)
response = self.call_view("un_flag_abuse_for_thread", mock_is_forum_v2_enabled, mock_request)
self._assert_json_response_contains_group_info(response)
def test_pin(self, mock_request):
def test_pin(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
"pin_thread",
mock_is_forum_v2_enabled,
mock_request,
user=self.moderator
)
self._assert_json_response_contains_group_info(response)
response = self.call_view(
"un_pin_thread",
mock_is_forum_v2_enabled,
mock_request,
user=self.moderator
)
self._assert_json_response_contains_group_info(response)
def test_openclose(self, mock_request):
def test_openclose(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
"openclose_thread",
mock_is_forum_v2_enabled,
mock_request,
user=self.moderator
)
@@ -280,10 +309,11 @@ class ViewsTestCaseMixin:
data["depth"] = 0
self._set_mock_request_data(mock_request, data)
def create_thread_helper(self, mock_request, extra_request_data=None, extra_response_data=None):
def create_thread_helper(self, mock_is_forum_v2_enabled, mock_request, extra_request_data=None, extra_response_data=None):
"""
Issues a request to create a thread and verifies the result.
"""
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"thread_type": "discussion",
"title": "Hello",
@@ -350,10 +380,11 @@ class ViewsTestCaseMixin:
)
assert response.status_code == 200
def update_thread_helper(self, mock_request):
def update_thread_helper(self, mock_is_forum_v2_enabled, mock_request):
"""
Issues a request to update a thread and verifies the result.
"""
mock_is_forum_v2_enabled.return_value = False
self._setup_mock_request(mock_request)
# Mock out saving in order to test that content is correctly
# updated. Otherwise, the call to thread.save() receives the
@@ -376,6 +407,7 @@ class ViewsTestCaseMixin:
@ddt.ddt
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
@disable_signal(views, 'thread_created')
@disable_signal(views, 'thread_edited')
class ViewsQueryCountTestCase(
@@ -393,6 +425,11 @@ class ViewsQueryCountTestCase(
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def count_queries(func): # pylint: disable=no-self-argument
"""
@@ -414,22 +451,23 @@ class ViewsQueryCountTestCase(
)
@ddt.unpack
@count_queries
def test_create_thread(self, mock_request):
self.create_thread_helper(mock_request)
def test_create_thread(self, mock_is_forum_v2_enabled, mock_request):
self.create_thread_helper(mock_is_forum_v2_enabled, mock_request)
@ddt.data(
(ModuleStoreEnum.Type.split, 3, 6, 41),
)
@ddt.unpack
@count_queries
def test_update_thread(self, mock_request):
self.update_thread_helper(mock_request)
def test_update_thread(self, mock_is_forum_v2_enabled, mock_request):
self.update_thread_helper(mock_is_forum_v2_enabled, mock_request)
@ddt.ddt
@disable_signal(views, 'comment_flagged')
@disable_signal(views, 'thread_flagged')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class ViewsTestCase(
ForumsEnableMixin,
UrlResetMixin,
@@ -464,7 +502,16 @@ class ViewsTestCase(
# so we need to call super.setUp() which reloads urls.py (because
# of the UrlResetMixin)
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
# Patch the comment client user save method so it does not try
# to create a new cc user when creating a django user
with patch('common.djangoapps.student.models.user.cc.User.save'):
@@ -497,11 +544,11 @@ class ViewsTestCase(
with self.assert_signal_sent(views, signal, sender=None, user=user, exclude_args=('post',)):
yield
def test_create_thread(self, mock_request):
def test_create_thread(self, mock_is_forum_v2_enabled, mock_request):
with self.assert_discussion_signals('thread_created'):
self.create_thread_helper(mock_request)
self.create_thread_helper(mock_is_forum_v2_enabled, mock_request)
def test_create_thread_standalone(self, mock_request):
def test_create_thread_standalone(self, mock_is_forum_v2_enabled, mock_request):
team = CourseTeamFactory.create(
name="A Team",
course_id=self.course_id,
@@ -513,15 +560,15 @@ class ViewsTestCase(
team.add_user(self.student)
# create_thread_helper verifies that extra data are passed through to the comments service
self.create_thread_helper(mock_request, extra_response_data={'context': ThreadContext.STANDALONE})
self.create_thread_helper(mock_is_forum_v2_enabled, mock_request, extra_response_data={'context': ThreadContext.STANDALONE})
@ddt.data(
('follow_thread', 'thread_followed'),
('unfollow_thread', 'thread_unfollowed'),
)
@ddt.unpack
def test_follow_unfollow_thread_signals(self, view_name, signal, mock_request):
self.create_thread_helper(mock_request)
def test_follow_unfollow_thread_signals(self, view_name, signal, mock_is_forum_v2_enabled, mock_request):
self.create_thread_helper(mock_is_forum_v2_enabled, mock_request)
with self.assert_discussion_signals(signal):
response = self.client.post(
@@ -532,7 +579,8 @@ class ViewsTestCase(
)
assert response.status_code == 200
def test_delete_thread(self, mock_request):
def test_delete_thread(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"user_id": str(self.student.id),
"closed": False,
@@ -551,7 +599,8 @@ class ViewsTestCase(
assert response.status_code == 200
assert mock_request.called
def test_delete_comment(self, mock_request):
def test_delete_comment(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"user_id": str(self.student.id),
"closed": False,
@@ -573,12 +622,13 @@ class ViewsTestCase(
assert args[0] == 'delete'
assert args[1].endswith(f"/{test_comment_id}")
def _test_request_error(self, view_name, view_kwargs, data, mock_request):
def _test_request_error(self, view_name, view_kwargs, data, mock_is_forum_v2_enabled, mock_request):
"""
Submit a request against the given view with the given data and ensure
that the result is a 400 error and that no data was posted using
mock_request
"""
mock_is_forum_v2_enabled.return_value = False
self._setup_mock_request(mock_request, include_depth=(view_name == "create_sub_comment"))
response = self.client.post(reverse(view_name, kwargs=view_kwargs), data=data)
@@ -586,87 +636,97 @@ class ViewsTestCase(
for call in mock_request.call_args_list:
assert call[0][0].lower() == 'get'
def test_create_thread_no_title(self, mock_request):
def test_create_thread_no_title(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_thread",
{"commentable_id": "dummy", "course_id": str(self.course_id)},
{"body": "foo"},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_thread_empty_title(self, mock_request):
def test_create_thread_empty_title(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_thread",
{"commentable_id": "dummy", "course_id": str(self.course_id)},
{"body": "foo", "title": " "},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_thread_no_body(self, mock_request):
def test_create_thread_no_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_thread",
{"commentable_id": "dummy", "course_id": str(self.course_id)},
{"title": "foo"},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_thread_empty_body(self, mock_request):
def test_create_thread_empty_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_thread",
{"commentable_id": "dummy", "course_id": str(self.course_id)},
{"body": " ", "title": "foo"},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_thread_no_title(self, mock_request):
def test_update_thread_no_title(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_thread",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{"body": "foo"},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_thread_empty_title(self, mock_request):
def test_update_thread_empty_title(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_thread",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{"body": "foo", "title": " "},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_thread_no_body(self, mock_request):
def test_update_thread_no_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_thread",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{"title": "foo"},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_thread_empty_body(self, mock_request):
def test_update_thread_empty_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_thread",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{"body": " ", "title": "foo"},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_thread_course_topic(self, mock_request):
def test_update_thread_course_topic(self, mock_is_forum_v2_enabled, mock_request):
with self.assert_discussion_signals('thread_edited'):
self.update_thread_helper(mock_request)
self.update_thread_helper(mock_is_forum_v2_enabled, mock_request)
@patch(
'lms.djangoapps.discussion.django_comment_client.utils.get_discussion_categories_ids',
return_value=["test_commentable"],
)
def test_update_thread_wrong_commentable_id(self, mock_get_discussion_id_map, mock_request):
def test_update_thread_wrong_commentable_id(self, mock_get_discussion_id_map, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_thread",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{"body": "foo", "title": "foo", "commentable_id": "wrong_commentable"},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_comment(self, mock_request):
def test_create_comment(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._setup_mock_request(mock_request)
with self.assert_discussion_signals('comment_created'):
response = self.client.post(
@@ -678,55 +738,62 @@ class ViewsTestCase(
)
assert response.status_code == 200
def test_create_comment_no_body(self, mock_request):
def test_create_comment_no_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_comment",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_comment_empty_body(self, mock_request):
def test_create_comment_empty_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_comment",
{"thread_id": "dummy", "course_id": str(self.course_id)},
{"body": " "},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_sub_comment_no_body(self, mock_request):
def test_create_sub_comment_no_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_sub_comment",
{"comment_id": "dummy", "course_id": str(self.course_id)},
{},
mock_is_forum_v2_enabled,
mock_request
)
def test_create_sub_comment_empty_body(self, mock_request):
def test_create_sub_comment_empty_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"create_sub_comment",
{"comment_id": "dummy", "course_id": str(self.course_id)},
{"body": " "},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_comment_no_body(self, mock_request):
def test_update_comment_no_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_comment",
{"comment_id": "dummy", "course_id": str(self.course_id)},
{},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_comment_empty_body(self, mock_request):
def test_update_comment_empty_body(self, mock_is_forum_v2_enabled, mock_request):
self._test_request_error(
"update_comment",
{"comment_id": "dummy", "course_id": str(self.course_id)},
{"body": " "},
mock_is_forum_v2_enabled,
mock_request
)
def test_update_comment_basic(self, mock_request):
def test_update_comment_basic(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._setup_mock_request(mock_request)
comment_id = "test_comment_id"
updated_body = "updated body"
@@ -748,13 +815,14 @@ class ViewsTestCase(
data={"body": updated_body}
)
def test_flag_thread_open(self, mock_request):
self.flag_thread(mock_request, False)
def test_flag_thread_open(self, mock_is_forum_v2_enabled, mock_request):
self.flag_thread(mock_is_forum_v2_enabled, mock_request, False)
def test_flag_thread_close(self, mock_request):
self.flag_thread(mock_request, True)
def test_flag_thread_close(self, mock_is_forum_v2_enabled, mock_request):
self.flag_thread(mock_is_forum_v2_enabled, mock_request, True)
def flag_thread(self, mock_request, is_closed):
def flag_thread(self, mock_is_forum_v2_enabled, mock_request, is_closed):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"title": "Hello",
"body": "this is a post",
@@ -826,13 +894,14 @@ class ViewsTestCase(
assert response.status_code == 200
def test_un_flag_thread_open(self, mock_request):
self.un_flag_thread(mock_request, False)
def test_un_flag_thread_open(self, mock_is_forum_v2_enabled, mock_request):
self.un_flag_thread(mock_is_forum_v2_enabled, mock_request, False)
def test_un_flag_thread_close(self, mock_request):
self.un_flag_thread(mock_request, True)
def test_un_flag_thread_close(self, mock_is_forum_v2_enabled, mock_request):
self.un_flag_thread(mock_is_forum_v2_enabled, mock_request, True)
def un_flag_thread(self, mock_request, is_closed):
def un_flag_thread(self, mock_is_forum_v2_enabled, mock_request, is_closed):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"title": "Hello",
"body": "this is a post",
@@ -905,13 +974,14 @@ class ViewsTestCase(
assert response.status_code == 200
def test_flag_comment_open(self, mock_request):
self.flag_comment(mock_request, False)
def test_flag_comment_open(self, mock_is_forum_v2_enabled, mock_request):
self.flag_comment(mock_is_forum_v2_enabled, mock_request, False)
def test_flag_comment_close(self, mock_request):
self.flag_comment(mock_request, True)
def test_flag_comment_close(self, mock_is_forum_v2_enabled, mock_request):
self.flag_comment(mock_is_forum_v2_enabled, mock_request, True)
def flag_comment(self, mock_request, is_closed):
def flag_comment(self, mock_is_forum_v2_enabled, mock_request, is_closed):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"body": "this is a comment",
"course_id": "MITx/999/Robot_Super_Course",
@@ -976,13 +1046,14 @@ class ViewsTestCase(
assert response.status_code == 200
def test_un_flag_comment_open(self, mock_request):
self.un_flag_comment(mock_request, False)
def test_un_flag_comment_open(self, mock_is_forum_v2_enabled, mock_request):
self.un_flag_comment(mock_is_forum_v2_enabled, mock_request, False)
def test_un_flag_comment_close(self, mock_request):
self.un_flag_comment(mock_request, True)
def test_un_flag_comment_close(self, mock_is_forum_v2_enabled, mock_request):
self.un_flag_comment(mock_is_forum_v2_enabled, mock_request, True)
def un_flag_comment(self, mock_request, is_closed):
def un_flag_comment(self, mock_is_forum_v2_enabled, mock_request, is_closed):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"body": "this is a comment",
"course_id": "MITx/999/Robot_Super_Course",
@@ -1054,7 +1125,8 @@ class ViewsTestCase(
('downvote_comment', 'comment_id', 'comment_voted')
)
@ddt.unpack
def test_voting(self, view_name, item_id, signal, mock_request):
def test_voting(self, view_name, item_id, signal, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._setup_mock_request(mock_request)
with self.assert_discussion_signals(signal):
response = self.client.post(
@@ -1065,7 +1137,8 @@ class ViewsTestCase(
)
assert response.status_code == 200
def test_endorse_comment(self, mock_request):
def test_endorse_comment(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._setup_mock_request(mock_request)
self.client.login(username=self.moderator.username, password=self.password)
with self.assert_discussion_signals('comment_endorsed', user=self.moderator):
@@ -1079,6 +1152,7 @@ class ViewsTestCase(
@patch("openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request", autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
@disable_signal(views, 'comment_endorsed')
class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase, MockRequestSetupMixin):
@@ -1106,8 +1180,19 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def test_pin_thread_as_student(self, mock_request):
def test_pin_thread_as_student(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {})
self.client.login(username=self.student.username, password=self.password)
response = self.client.post(
@@ -1115,7 +1200,8 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
)
assert response.status_code == 401
def test_pin_thread_as_moderator(self, mock_request):
def test_pin_thread_as_moderator(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {})
self.client.login(username=self.moderator.username, password=self.password)
response = self.client.post(
@@ -1123,7 +1209,8 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
)
assert response.status_code == 200
def test_un_pin_thread_as_student(self, mock_request):
def test_un_pin_thread_as_student(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {})
self.client.login(username=self.student.username, password=self.password)
response = self.client.post(
@@ -1131,7 +1218,8 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
)
assert response.status_code == 401
def test_un_pin_thread_as_moderator(self, mock_request):
def test_un_pin_thread_as_moderator(self, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {})
self.client.login(username=self.moderator.username, password=self.password)
response = self.client.post(
@@ -1139,7 +1227,7 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
)
assert response.status_code == 200
def _set_mock_request_thread_and_comment(self, mock_request, thread_data, comment_data):
def _set_mock_request_thread_and_comment(self, mock_is_forum_v2_enabled, mock_request, thread_data, comment_data):
def handle_request(*args, **kwargs):
url = args[1]
if "/threads/" in url:
@@ -1148,10 +1236,12 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
return self._create_response_mock(comment_data)
else:
raise ArgumentError("Bad url to mock request")
mock_is_forum_v2_enabled.return_value = False
mock_request.side_effect = handle_request
def test_endorse_response_as_staff(self, mock_request):
def test_endorse_response_as_staff(self, mock_is_forum_v2_enabled, mock_request):
self._set_mock_request_thread_and_comment(
mock_is_forum_v2_enabled,
mock_request,
{"type": "thread", "thread_type": "question", "user_id": str(self.student.id), "commentable_id": "course"},
{"type": "comment", "thread_id": "dummy"}
@@ -1162,8 +1252,9 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
)
assert response.status_code == 200
def test_endorse_response_as_student(self, mock_request):
def test_endorse_response_as_student(self, mock_is_forum_v2_enabled, mock_request):
self._set_mock_request_thread_and_comment(
mock_is_forum_v2_enabled,
mock_request,
{"type": "thread", "thread_type": "question",
"user_id": str(self.moderator.id), "commentable_id": "course"},
@@ -1175,8 +1266,9 @@ class ViewPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleStor
)
assert response.status_code == 401
def test_endorse_response_as_student_question_author(self, mock_request):
def test_endorse_response_as_student_question_author(self, mock_is_forum_v2_enabled, mock_request):
self._set_mock_request_thread_and_comment(
mock_is_forum_v2_enabled,
mock_request,
{"type": "thread", "thread_type": "question", "user_id": str(self.student.id), "commentable_id": "course"},
{"type": "comment", "thread_id": "dummy"}
@@ -1209,10 +1301,12 @@ class CreateThreadUnicodeTestCase(
CourseEnrollmentFactory(user=cls.student, course_id=cls.course.id)
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def _test_unicode_data(self, text, mock_request,):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def _test_unicode_data(self, text, mock_is_forum_v2_enabled, mock_request,):
"""
Test to make sure unicode data in a thread doesn't break it.
"""
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {})
request = RequestFactory().post("dummy_url", {"thread_type": "discussion", "body": text, "title": text})
request.user = self.student
@@ -1235,6 +1329,13 @@ class UpdateThreadUnicodeTestCase(
UnicodeTestMixin,
MockRequestSetupMixin
):
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def setUpClass(cls):
@@ -1255,7 +1356,9 @@ class UpdateThreadUnicodeTestCase(
return_value=["test_commentable"],
)
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def _test_unicode_data(self, text, mock_request, mock_get_discussion_id_map):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def _test_unicode_data(self, text, mock_is_forum_v2_enabled, mock_request, mock_get_discussion_id_map):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"user_id": str(self.student.id),
"closed": False,
@@ -1280,6 +1383,13 @@ class CreateCommentUnicodeTestCase(
UnicodeTestMixin,
MockRequestSetupMixin
):
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def setUpClass(cls):
@@ -1296,7 +1406,9 @@ class CreateCommentUnicodeTestCase(
CourseEnrollmentFactory(user=cls.student, course_id=cls.course.id)
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def _test_unicode_data(self, text, mock_request):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def _test_unicode_data(self, text, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
commentable_id = "non_team_dummy_id"
self._set_mock_request_data(mock_request, {
"closed": False,
@@ -1327,6 +1439,13 @@ class UpdateCommentUnicodeTestCase(
UnicodeTestMixin,
MockRequestSetupMixin
):
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def setUpClass(cls):
@@ -1343,7 +1462,9 @@ class UpdateCommentUnicodeTestCase(
CourseEnrollmentFactory(user=cls.student, course_id=cls.course.id)
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def _test_unicode_data(self, text, mock_request):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def _test_unicode_data(self, text, mock_is_forum_v2_enabled, mock_request):
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"user_id": str(self.student.id),
"closed": False,
@@ -1359,6 +1480,7 @@ class UpdateCommentUnicodeTestCase(
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class CommentActionTestCase(
MockRequestSetupMixin,
CohortedTestCase,
@@ -1367,11 +1489,18 @@ class CommentActionTestCase(
def call_view(
self,
view_name,
mock_is_forum_v2_enabled,
mock_request,
user=None,
post_params=None,
view_args=None
):
mock_is_forum_v2_enabled.return_value = False
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
self._set_mock_request_data(
mock_request,
{
@@ -1394,9 +1523,9 @@ class CommentActionTestCase(
**(view_args or {})
)
def test_flag(self, mock_request):
def test_flag(self, mock_is_forum_v2_enabled, mock_request):
with mock.patch('openedx.core.djangoapps.django_comment_common.signals.comment_flagged.send') as signal_mock:
self.call_view("flag_abuse_for_comment", mock_request)
self.call_view("flag_abuse_for_comment", mock_is_forum_v2_enabled, mock_request)
self.assertEqual(signal_mock.call_count, 1)
@@ -1410,6 +1539,14 @@ class CreateSubCommentUnicodeTestCase(
"""
Make sure comments under a response can handle unicode.
"""
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def setUpClass(cls):
# pylint: disable=super-method-not-called
@@ -1425,10 +1562,12 @@ class CreateSubCommentUnicodeTestCase(
CourseEnrollmentFactory(user=cls.student, course_id=cls.course.id)
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def _test_unicode_data(self, text, mock_request):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def _test_unicode_data(self, text, mock_is_forum_v2_enabled, mock_request):
"""
Create a comment with unicode in it.
"""
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"closed": False,
"depth": 1,
@@ -1453,6 +1592,7 @@ class CreateSubCommentUnicodeTestCase(
@ddt.ddt
@patch("openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request", autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
@disable_signal(views, 'thread_voted')
@disable_signal(views, 'thread_edited')
@disable_signal(views, 'comment_created')
@@ -1562,13 +1702,24 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
users=[cls.group_moderator, cls.cohorted]
)
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
def _setup_mock(self, user, mock_request, data):
def _setup_mock(self, user, mock_is_forum_v2_enabled, mock_request, data):
user = getattr(self, user)
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, data)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.client.login(username=user.username, password=self.password)
@ddt.data(
@@ -1593,7 +1744,7 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
('group_moderator', 'cohorted', 'course_commentable_id', 401, CourseDiscussionSettings.NONE)
)
@ddt.unpack
def test_update_thread(self, user, thread_author, commentable_id, status_code, division_scheme, mock_request):
def test_update_thread(self, user, thread_author, commentable_id, status_code, division_scheme, mock_is_forum_v2_enabled, mock_request):
"""
Verify that update_thread is limited to thread authors and privileged users (team membership does not matter).
"""
@@ -1603,7 +1754,7 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
thread_author = getattr(self, thread_author)
self._setup_mock(
user, mock_request, # user is the person making the request.
user, mock_is_forum_v2_enabled, mock_request, # user is the person making the request.
{
"user_id": str(thread_author.id),
"closed": False, "commentable_id": commentable_id,
@@ -1643,12 +1794,12 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
('group_moderator', 'cohorted', 'team_commentable_id', 401, CourseDiscussionSettings.NONE)
)
@ddt.unpack
def test_delete_comment(self, user, comment_author, commentable_id, status_code, division_scheme, mock_request):
def test_delete_comment(self, user, comment_author, commentable_id, status_code, division_scheme, mock_is_forum_v2_enabled, mock_request):
commentable_id = getattr(self, commentable_id)
comment_author = getattr(self, comment_author)
self.change_divided_discussion_settings(division_scheme)
self._setup_mock(user, mock_request, {
self._setup_mock(user, mock_is_forum_v2_enabled, mock_request, {
"closed": False,
"commentable_id": commentable_id,
"user_id": str(comment_author.id),
@@ -1671,12 +1822,12 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
@ddt.data(*ddt_permissions_args)
@ddt.unpack
def test_create_comment(self, user, commentable_id, status_code, mock_request):
def test_create_comment(self, user, commentable_id, status_code, mock_is_forum_v2_enabled, mock_request):
"""
Verify that create_comment is limited to members of the team or users with 'edit_content' permission.
"""
commentable_id = getattr(self, commentable_id)
self._setup_mock(user, mock_request, {"closed": False, "commentable_id": commentable_id})
self._setup_mock(user, mock_is_forum_v2_enabled, mock_request, {"closed": False, "commentable_id": commentable_id})
response = self.client.post(
reverse(
@@ -1692,13 +1843,13 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
@ddt.data(*ddt_permissions_args)
@ddt.unpack
def test_create_sub_comment(self, user, commentable_id, status_code, mock_request):
def test_create_sub_comment(self, user, commentable_id, status_code, mock_is_forum_v2_enabled, mock_request):
"""
Verify that create_subcomment is limited to members of the team or users with 'edit_content' permission.
"""
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
user, mock_is_forum_v2_enabled, mock_request,
{"closed": False, "commentable_id": commentable_id, "thread_id": "dummy_thread"},
)
response = self.client.post(
@@ -1715,14 +1866,14 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
@ddt.data(*ddt_permissions_args)
@ddt.unpack
def test_comment_actions(self, user, commentable_id, status_code, mock_request):
def test_comment_actions(self, user, commentable_id, status_code, mock_is_forum_v2_enabled, mock_request):
"""
Verify that voting and flagging of comments is limited to members of the team or users with
'edit_content' permission.
"""
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
user, mock_is_forum_v2_enabled, mock_request,
{
"closed": False,
"commentable_id": commentable_id,
@@ -1742,14 +1893,14 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
@ddt.data(*ddt_permissions_args)
@ddt.unpack
def test_threads_actions(self, user, commentable_id, status_code, mock_request):
def test_threads_actions(self, user, commentable_id, status_code, mock_is_forum_v2_enabled, mock_request):
"""
Verify that voting, flagging, and following of threads is limited to members of the team or users with
'edit_content' permission.
"""
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
user, mock_is_forum_v2_enabled, mock_request,
{"closed": False, "commentable_id": commentable_id, "body": "dummy body", "course_id": str(self.course.id)}
)
for action in ["upvote_thread", "downvote_thread", "un_flag_abuse_for_thread", "flag_abuse_for_thread",
@@ -1772,6 +1923,19 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
"""
Forum actions are expected to launch analytics events. Test these here.
"""
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def setUpClass(cls):
# pylint: disable=super-method-not-called
@@ -1791,12 +1955,14 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
@patch('eventtracking.tracker.emit')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_response_event(self, mock_request, mock_emit):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_response_event(self, mock_is_forum_v2_enabled, mock_request, mock_emit):
"""
Check to make sure an event is fired when a user responds to a thread.
"""
event_receiver = Mock()
FORUM_THREAD_RESPONSE_CREATED.connect(event_receiver)
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"closed": False,
"commentable_id": 'test_commentable_id',
@@ -1833,12 +1999,14 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
@patch('eventtracking.tracker.emit')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_comment_event(self, mock_request, mock_emit):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_comment_event(self, mock_is_forum_v2_enabled, mock_request, mock_emit):
"""
Ensure an event is fired when someone comments on a response.
"""
event_receiver = Mock()
FORUM_RESPONSE_COMMENT_CREATED.connect(event_receiver)
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"closed": False,
"depth": 1,
@@ -1875,6 +2043,7 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
@patch('eventtracking.tracker.emit')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
@ddt.data((
'create_thread',
'edx.forum.thread.created', {
@@ -1896,7 +2065,7 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
{'comment_id': 'dummy_comment_id'}
))
@ddt.unpack
def test_team_events(self, view_name, event_name, view_data, view_kwargs, mock_request, mock_emit):
def test_team_events(self, view_name, event_name, view_data, view_kwargs, mock_is_forum_v2_enabled, mock_request, mock_emit):
user = self.student
team = CourseTeamFactory.create(discussion_topic_id=TEAM_COMMENTABLE_ID)
CourseTeamMembershipFactory.create(team=team, user=user)
@@ -1905,6 +2074,7 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
forum_event = views.TRACKING_LOG_TO_EVENT_MAPS.get(event_name)
forum_event.connect(event_receiver)
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
'closed': False,
'commentable_id': TEAM_COMMENTABLE_ID,
@@ -1943,9 +2113,11 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
@ddt.unpack
@patch('eventtracking.tracker.emit')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_thread_voted_event(self, view_name, obj_id_name, obj_type, mock_request, mock_emit):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_thread_voted_event(self, view_name, obj_id_name, obj_type, mock_is_forum_v2_enabled, mock_request, mock_emit):
undo = view_name.startswith('undo')
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
'closed': False,
'commentable_id': 'test_commentable_id',
@@ -1971,11 +2143,13 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
@ddt.data('follow_thread', 'unfollow_thread',)
@patch('eventtracking.tracker.emit')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_thread_followed_event(self, view_name, mock_request, mock_emit):
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_thread_followed_event(self, view_name, mock_is_forum_v2_enabled, mock_request, mock_emit):
event_receiver = Mock()
for signal in views.TRACKING_LOG_TO_EVENT_MAPS.values():
signal.connect(event_receiver)
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
'closed': False,
'commentable_id': 'test_commentable_id',
@@ -2025,10 +2199,11 @@ class UsersEndpointTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockRe
cls.other_user = UserFactory.create(username="other")
CourseEnrollmentFactory(user=cls.other_user, course_id=cls.course.id)
def set_post_counts(self, mock_request, threads_count=1, comments_count=1):
def set_post_counts(self, mock_is_forum_v2_enabled, mock_request, threads_count=1, comments_count=1):
"""
sets up a mock response from the comments service for getting post counts for our other_user
"""
mock_is_forum_v2_enabled.return_value = False
self._set_mock_request_data(mock_request, {
"threads_count": threads_count,
"comments_count": comments_count,
@@ -2042,15 +2217,17 @@ class UsersEndpointTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockRe
return views.users(request, course_id=str(course_id))
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_finds_exact_match(self, mock_request):
self.set_post_counts(mock_request)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_finds_exact_match(self, mock_is_forum_v2_enabled, mock_request):
self.set_post_counts(mock_is_forum_v2_enabled, mock_request)
response = self.make_request(username="other")
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8'))['users'] == [{'id': self.other_user.id, 'username': self.other_user.username}]
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_finds_no_match(self, mock_request):
self.set_post_counts(mock_request)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_finds_no_match(self, mock_is_forum_v2_enabled, mock_request):
self.set_post_counts(mock_is_forum_v2_enabled, mock_request)
response = self.make_request(username="othor")
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8'))['users'] == []
@@ -2086,8 +2263,9 @@ class UsersEndpointTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockRe
assert 'users' not in content
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
def test_requires_matched_user_has_forum_content(self, mock_request):
self.set_post_counts(mock_request, 0, 0)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
def test_requires_matched_user_has_forum_content(self, mock_is_forum_v2_enabled, mock_request):
self.set_post_counts(mock_is_forum_v2_enabled, mock_request, 0, 0)
response = self.make_request(username="other")
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8'))['users'] == []

View File

@@ -562,7 +562,6 @@ def create_thread(request, course_id, commentable_id):
params['context'] = ThreadContext.STANDALONE
else:
params['context'] = ThreadContext.COURSE
thread = cc.Thread(**params)
# Divide the thread if required

View File

@@ -60,51 +60,76 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
Provides test cases to verify that views pass the correct `group_id` to
the comments service when requesting content in cohorted discussions.
"""
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
def call_view(self, mock_is_forum_v2_enabled, mock_request, commentable_id, user, group_id, pass_group_id=True):
"""
Call the view for the implementing test class, constructing a request
from the parameters.
"""
pass # lint-amnesty, pylint: disable=unnecessary-pass
def test_cohorted_topic_student_without_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.student, '', pass_group_id=False)
def test_cohorted_topic_student_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.student, '', pass_group_id=False)
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
def test_cohorted_topic_student_none_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.student, "")
def test_cohorted_topic_student_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.student, "")
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
def test_cohorted_topic_student_with_own_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.student, self.student_cohort.id)
def test_cohorted_topic_student_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.student, self.student_cohort.id)
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
def test_cohorted_topic_student_with_other_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.student, self.moderator_cohort.id)
def test_cohorted_topic_student_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
self.moderator_cohort.id
)
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
def test_cohorted_topic_moderator_without_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.moderator, '', pass_group_id=False)
def test_cohorted_topic_moderator_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.moderator,
'',
pass_group_id=False
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_cohorted_topic_moderator_none_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.moderator, "")
def test_cohorted_topic_moderator_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.moderator, "")
self._assert_comments_service_called_without_group_id(mock_request)
def test_cohorted_topic_moderator_with_own_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.moderator, self.moderator_cohort.id)
def test_cohorted_topic_moderator_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.moderator,
self.moderator_cohort.id
)
self._assert_comments_service_called_with_group_id(mock_request, self.moderator_cohort.id)
def test_cohorted_topic_moderator_with_other_group_id(self, mock_request):
self.call_view(mock_request, "cohorted_topic", self.moderator, self.student_cohort.id)
def test_cohorted_topic_moderator_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.moderator,
self.student_cohort.id
)
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_is_forum_v2_enabled, mock_request):
invalid_id = self.student_cohort.id + self.moderator_cohort.id
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id) # lint-amnesty, pylint: disable=assignment-from-no-return
response = self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.moderator, invalid_id) # lint-amnesty, pylint: disable=assignment-from-no-return
assert response.status_code == 500
def test_cohorted_topic_enrollment_track_invalid_group_id(self, mock_request):
def test_cohorted_topic_enrollment_track_invalid_group_id(self, mock_is_forum_v2_enabled, mock_request):
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
discussion_settings = CourseDiscussionSettings.get(self.course.id)
@@ -115,7 +140,7 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
})
invalid_id = -1000
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id) # lint-amnesty, pylint: disable=assignment-from-no-return
response = self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.moderator, invalid_id) # lint-amnesty, pylint: disable=assignment-from-no-return
assert response.status_code == 500
@@ -124,57 +149,95 @@ class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
Provides test cases to verify that views pass the correct `group_id` to
the comments service when requesting content in non-cohorted discussions.
"""
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
def call_view(self, mock_is_forum_v2_enabled, mock_request, commentable_id, user, group_id, pass_group_id=True):
"""
Call the view for the implementing test class, constructing a request
from the parameters.
"""
pass # lint-amnesty, pylint: disable=unnecessary-pass
def test_non_cohorted_topic_student_without_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.student, '', pass_group_id=False)
def test_non_cohorted_topic_student_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"non_cohorted_topic",
self.student,
'',
pass_group_id=False
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_student_none_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.student, '')
def test_non_cohorted_topic_student_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(mock_is_forum_v2_enabled, mock_request, "non_cohorted_topic", self.student, '')
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_student_with_own_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.student, self.student_cohort.id)
def test_non_cohorted_topic_student_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"non_cohorted_topic",
self.student,
self.student_cohort.id
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_student_with_other_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.student, self.moderator_cohort.id)
def test_non_cohorted_topic_student_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"non_cohorted_topic",
self.student,
self.moderator_cohort.id
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_moderator_without_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.moderator, '', pass_group_id=False)
def test_non_cohorted_topic_moderator_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"non_cohorted_topic",
self.moderator,
"",
pass_group_id=False,
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_moderator_none_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.moderator, '')
def test_non_cohorted_topic_moderator_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(mock_is_forum_v2_enabled, mock_request, "non_cohorted_topic", self.moderator, '')
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_moderator_with_own_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.moderator, self.moderator_cohort.id)
def test_non_cohorted_topic_moderator_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"non_cohorted_topic",
self.moderator,
self.moderator_cohort.id,
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_moderator_with_other_group_id(self, mock_request):
self.call_view(mock_request, "non_cohorted_topic", self.moderator, self.student_cohort.id)
def test_non_cohorted_topic_moderator_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"non_cohorted_topic",
self.moderator,
self.student_cohort.id,
)
self._assert_comments_service_called_without_group_id(mock_request)
def test_non_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
def test_non_cohorted_topic_moderator_with_invalid_group_id(self, mock_is_forum_v2_enabled, mock_request):
invalid_id = self.student_cohort.id + self.moderator_cohort.id
self.call_view(mock_request, "non_cohorted_topic", self.moderator, invalid_id)
self.call_view(mock_is_forum_v2_enabled, mock_request, "non_cohorted_topic", self.moderator, invalid_id)
self._assert_comments_service_called_without_group_id(mock_request)
def test_team_discussion_id_not_cohorted(self, mock_request):
def test_team_discussion_id_not_cohorted(self, mock_is_forum_v2_enabled, mock_request):
team = CourseTeamFactory(
course_id=self.course.id,
topic_id='topic-id'
)
team.add_user(self.student)
self.call_view(mock_request, team.discussion_topic_id, self.student, '')
self.call_view(mock_is_forum_v2_enabled, mock_request, team.discussion_topic_id, self.student, '')
self._assert_comments_service_called_without_group_id(mock_request)

View File

@@ -199,7 +199,7 @@ def _get_course(course_key: CourseKey, user: User, check_tab: bool = True) -> Co
return course
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None, course_id=None):
"""
Retrieve the given thread and build a serializer context for it, returning
both. This function also enforces access control for the thread (checking
@@ -213,7 +213,7 @@ def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
retrieve_kwargs["with_responses"] = False
if "mark_as_read" not in retrieve_kwargs:
retrieve_kwargs["mark_as_read"] = False
cc_thread = Thread(id=thread_id).retrieve(**retrieve_kwargs)
cc_thread = Thread(id=thread_id).retrieve(course_id=course_id, **retrieve_kwargs)
course_key = CourseKey.from_string(cc_thread["course_id"])
course = _get_course(course_key, request.user)
context = get_context(course, request, cc_thread)
@@ -1645,7 +1645,8 @@ def get_thread(request, thread_id, requested_fields=None, course_id=None):
retrieve_kwargs={
"with_responses": True,
"user_id": str(request.user.id),
}
},
course_id=course_id,
)
if course_id and course_id != cc_thread.course_id:
raise ThreadNotFoundError("Thread not found.")

View File

@@ -202,7 +202,7 @@ class DiscussionNotificationSender:
while has_more_subscribers:
subscribers = Subscription.fetch(self.thread.id, query_params={'page': page})
subscribers = Subscription.fetch(self.thread.id, self.course.id, query_params={'page': page})
if page <= subscribers.num_pages:
for subscriber in subscribers.collection:
# Check if the subscriber is not the thread creator or response creator

View File

@@ -68,7 +68,7 @@ def get_context(course, request, thread=None):
moderator_user_ids = get_moderator_users_list(course.id)
ta_user_ids = get_course_ta_users_list(course.id)
requester = request.user
cc_requester = CommentClientUser.from_django_user(requester).retrieve()
cc_requester = CommentClientUser.from_django_user(requester).retrieve(course_id=course.id)
cc_requester["course_id"] = course.id
course_discussion_settings = CourseDiscussionSettings.get(course.id)
is_global_staff = GlobalStaff().has_user(requester)

View File

@@ -1248,6 +1248,22 @@ class GetCommentListTest(ForumsEnableMixin, CommentsServiceMockMixin, SharedModu
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.maxDiff = None # pylint: disable=invalid-name
self.user = UserFactory.create()
self.register_get_user_response(self.user)
@@ -1872,6 +1888,12 @@ class CreateThreadTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/test_path")
@@ -2198,6 +2220,22 @@ class CreateCommentTest(
self.course = CourseFactory.create()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/test_path")
@@ -2589,6 +2627,17 @@ class UpdateThreadTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
@@ -3153,6 +3202,22 @@ class UpdateCommentTest(
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/test_path")
@@ -3670,6 +3735,22 @@ class DeleteThreadTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/test_path")
@@ -3823,6 +3904,22 @@ class DeleteCommentTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/test_path")
@@ -3991,6 +4088,17 @@ class RetrieveThreadTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/test_path")

View File

@@ -54,6 +54,12 @@ class SerializerTestMixin(ForumsEnableMixin, CommentsServiceMockMixin, UrlResetM
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.maxDiff = None # pylint: disable=invalid-name
self.user = UserFactory.create()
self.register_get_user_response(self.user)
@@ -571,6 +577,12 @@ class ThreadSerializerDeserializationTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/dummy")
@@ -802,6 +814,22 @@ class CommentSerializerDeserializationTest(ForumsEnableMixin, CommentsServiceMoc
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create()
self.register_get_user_response(self.user)
self.request = RequestFactory().get("/dummy")

View File

@@ -58,10 +58,27 @@ class TestNewThreadCreatedNotification(DiscussionAPIViewTestMixin, ModuleStoreTe
Setup test case
"""
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
# Creating a course
self.course = CourseFactory.create()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread",
return_value=self.course.id
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment",
return_value=self.course.id
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
# Creating relative discussion and cohort settings
CourseCohortsSettings.objects.create(course_id=str(self.course.id))
CourseDiscussionSettings.objects.create(course_id=str(self.course.id), _divided_discussions='[]')
@@ -250,8 +267,26 @@ class TestSendResponseNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestC
super().setUp()
httpretty.reset()
httpretty.enable()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread",
return_value=self.course.id
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment",
return_value=self.course.id
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
self.user_1 = UserFactory.create()
CourseEnrollment.enroll(self.user_1, self.course.id)
self.user_2 = UserFactory.create()
@@ -536,8 +571,26 @@ class TestSendCommentNotification(DiscussionAPIViewTestMixin, ModuleStoreTestCas
super().setUp()
httpretty.reset()
httpretty.enable()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread",
return_value=self.course.id
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment",
return_value=self.course.id
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
self.user_1 = UserFactory.create()
CourseEnrollment.enroll(self.user_1, self.course.id)
self.user_2 = UserFactory.create()
@@ -603,8 +656,26 @@ class TestResponseEndorsedNotifications(DiscussionAPIViewTestMixin, ModuleStoreT
super().setUp()
httpretty.reset()
httpretty.enable()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread",
return_value=self.course.id
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment",
return_value=self.course.id
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
self.user_1 = UserFactory.create()
CourseEnrollment.enroll(self.user_1, self.course.id)
self.user_2 = UserFactory.create()

View File

@@ -171,6 +171,12 @@ class UploadFileViewTest(ForumsEnableMixin, CommentsServiceMockMixin, UrlResetMi
self.user = UserFactory.create(password=self.TEST_PASSWORD)
self.course = CourseFactory.create(org='a', course='b', run='c', start=datetime.now(UTC))
self.url = reverse("upload_file", kwargs={"course_id": str(self.course.id)})
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def user_login(self):
"""
@@ -301,6 +307,7 @@ class UploadFileViewTest(ForumsEnableMixin, CommentsServiceMockMixin, UrlResetMi
@ddt.ddt
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_FORUM_V2": False})
class CommentViewSetListByUserTest(
ForumsEnableMixin,
CommentsServiceMockMixin,
@@ -319,6 +326,12 @@ class CommentViewSetListByUserTest(
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.user = UserFactory.create(password=self.TEST_PASSWORD)
self.register_get_user_response(self.user)
@@ -500,6 +513,12 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def setUp(self):
super().setUp()
self.url = reverse("discussion_course", kwargs={"course_id": str(self.course.id)})
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def test_404(self):
response = self.client.get(
@@ -561,6 +580,12 @@ class RetireViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
self.superuser_client = APIClient()
self.retired_username = get_retired_username_by_username(self.user.username)
self.url = reverse("retire_discussion_user")
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def assert_response_correct(self, response, expected_status, expected_content):
"""
@@ -631,6 +656,12 @@ class ReplaceUsernamesViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
self.worker_client = APIClient()
self.new_username = "test_username_replacement"
self.url = reverse("replace_discussion_username")
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def assert_response_correct(self, response, expected_status, expected_content):
"""
@@ -733,6 +764,12 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, CommentsServiceMockMixin,
"courseware-3": {"discussion": 7, "question": 2},
}
self.register_get_course_commentable_counts_response(self.course.id, self.thread_counts_map)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def create_course(self, blocks_count, module_store, topics):
"""
@@ -988,6 +1025,12 @@ class CourseTopicsViewV3Test(DiscussionAPIViewTestMixin, CommentsServiceMockMixi
patcher.start()
self.addCleanup(patcher.stop)
self.url = reverse("course_topics_v3", kwargs={"course_id": str(self.course.id)})
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
response = self.client.get(self.url)
@@ -1024,6 +1067,12 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro
super().setUp()
self.author = UserFactory.create()
self.url = reverse("thread-list")
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def create_source_thread(self, overrides=None):
"""
@@ -1365,6 +1414,12 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def setUp(self):
super().setUp()
self.url = reverse("thread-list")
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
self.register_get_user_response(self.user)
@@ -1437,6 +1492,17 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
self.unsupported_media_type = JSONParser.media_type
super().setUp()
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
self.register_get_user_response(self.user)
@@ -1581,6 +1647,17 @@ class ThreadViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
super().setUp()
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
self.thread_id = "test_thread"
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
self.register_get_user_response(self.user)
@@ -1681,6 +1758,12 @@ class LearnerThreadViewAPITest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
]
self.url = reverse("discussion_learner_threads", kwargs={'course_id': str(self.course.id)})
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def update_thread(self, thread):
"""
@@ -1923,6 +2006,17 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr
self.url = reverse("comment-list")
self.thread_id = "test_thread"
self.storage = get_profile_image_storage()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def create_source_comment(self, overrides=None):
"""
@@ -2377,6 +2471,22 @@ class CommentViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
super().setUp()
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
self.comment_id = "test_comment"
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
self.register_get_user_response(self.user)
@@ -2416,6 +2526,23 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def setUp(self):
super().setUp()
self.url = reverse("comment-list")
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
self.register_get_user_response(self.user)
@@ -2518,6 +2645,22 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
httpretty.enable()
self.addCleanup(httpretty.reset)
self.addCleanup(httpretty.disable)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.register_get_user_response(self.user)
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
@@ -2640,6 +2783,22 @@ class ThreadViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase,
super().setUp()
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
self.thread_id = "test_thread"
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def test_basic(self):
self.register_get_user_response(self.user)
@@ -2693,6 +2852,22 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
self.thread_id = "test_thread"
self.comment_id = "test_comment"
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def make_comment_data(self, comment_id, parent_id=None, children=[]): # pylint: disable=W0102
"""
@@ -2838,6 +3013,12 @@ class CourseDiscussionSettingsAPIViewTest(APITestCase, UrlResetMixin, ModuleStor
self.path = reverse('discussion_course_settings', kwargs={'course_id': str(self.course.id)})
self.password = self.TEST_PASSWORD
self.user = UserFactory(username='staff', password=self.password, is_staff=True)
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
def _get_oauth_headers(self, user):
"""Return the OAuth headers for testing OAuth authentication"""
@@ -3127,6 +3308,12 @@ class CourseDiscussionRolesAPIViewTest(APITestCase, UrlResetMixin, ModuleStoreTe
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create(
org="x",
course="y",
@@ -3318,6 +3505,12 @@ class CourseActivityStatsTest(ForumsEnableMixin, UrlResetMixin, CommentsServiceM
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self) -> None:
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create()
self.course_key = str(self.course.id)
seed_permissions_roles(self.course.id)

View File

@@ -232,6 +232,22 @@ class TaskTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missin
thread_permalink = '/courses/discussion/dummy_discussion_id'
self.permalink_patcher = mock.patch('lms.djangoapps.discussion.tasks.permalink', return_value=thread_permalink)
self.mock_permalink = self.permalink_patcher.start()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def tearDown(self):
super().tearDown()

View File

@@ -4,6 +4,7 @@ Tests the forum notification views.
import json
import logging
from datetime import datetime
from unittest import mock
from unittest.mock import ANY, Mock, call, patch
import ddt
@@ -109,9 +110,20 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): # lint-amnest
config = ForumsConfig.current()
config.enabled = True
config.save()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
@patch('common.djangoapps.student.models.user.cc.User.from_django_user')
@patch('common.djangoapps.student.models.user.cc.User.active_threads')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.user.User.active_threads')
def test_user_profile_exception(self, mock_threads, mock_from_django_user):
# Mock the code that makes the HTTP requests to the cs_comment_service app
@@ -323,6 +335,17 @@ class SingleThreadTestCase(ForumsEnableMixin, ModuleStoreTestCase): # lint-amne
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})
self.student = UserFactory.create()
@@ -513,6 +536,20 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
Ensures the number of modulestore queries and number of sql queries are
independent of the number of responses retrieved for a given discussion thread.
"""
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
@ddt.data(
# split mongo: 3 queries, regardless of thread response size.
(False, 1, 2, 2, 21, 8),
@@ -582,6 +619,20 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
@patch('requests.request', autospec=True)
class SingleCohortedThreadTestCase(CohortedTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def _create_mock_cohorted_thread(self, mock_request): # lint-amnesty, pylint: disable=missing-function-docstring
mock_text = "dummy content"
mock_thread_id = "test_thread_id"
@@ -644,6 +695,20 @@ class SingleCohortedThreadTestCase(CohortedTestCase): # lint-amnesty, pylint: d
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
class SingleThreadAccessTestCase(CohortedTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def call_view(self, mock_request, commentable_id, user, group_id, thread_group_id=None, pass_group_id=True): # lint-amnesty, pylint: disable=missing-function-docstring
thread_id = "test_thread_id"
mock_request.side_effect = make_mock_request_impl(
@@ -746,6 +811,20 @@ class SingleThreadAccessTestCase(CohortedTestCase): # lint-amnesty, pylint: dis
class SingleThreadGroupIdTestCase(CohortedTestCase, GroupIdAssertionMixin): # lint-amnesty, pylint: disable=missing-class-docstring
cs_endpoint = "/threads/dummy_thread_id"
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True, is_ajax=False): # lint-amnesty, pylint: disable=missing-function-docstring
mock_request.side_effect = make_mock_request_impl(
course=self.course, text="dummy context", group_id=self.student_cohort.id
@@ -881,6 +960,22 @@ class SingleThreadContentGroupTestCase(ForumsEnableMixin, UrlResetMixin, Content
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def assert_can_access(self, user, discussion_id, thread_id, should_have_access):
"""
@@ -1046,6 +1141,7 @@ class InlineDiscussionContextTestCase(ForumsEnableMixin, ModuleStoreTestCase):
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class InlineDiscussionGroupIdTestCase( # lint-amnesty, pylint: disable=missing-class-docstring
CohortedTestCase,
CohortedTopicGroupIdTestMixin,
@@ -1056,8 +1152,22 @@ class InlineDiscussionGroupIdTestCase( # lint-amnesty, pylint: disable=missing-
def setUp(self):
super().setUp()
self.cohorted_commentable_id = 'cohorted_topic'
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
def call_view(
self,
mock_is_forum_v2_enabled,
mock_request,
commentable_id,
user,
group_id,
pass_group_id=True
): # pylint: disable=arguments-differ
mock_is_forum_v2_enabled.return_value = False
kwargs = {'commentable_id': self.cohorted_commentable_id}
if group_id:
# avoid causing a server error when the LMS chokes attempting
@@ -1084,8 +1194,9 @@ class InlineDiscussionGroupIdTestCase( # lint-amnesty, pylint: disable=missing-
commentable_id
)
def test_group_info_in_ajax_response(self, mock_request):
def test_group_info_in_ajax_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
self.cohorted_commentable_id,
self.student,
@@ -1097,10 +1208,29 @@ class InlineDiscussionGroupIdTestCase( # lint-amnesty, pylint: disable=missing-
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin): # lint-amnesty, pylint: disable=missing-class-docstring
cs_endpoint = "/threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True, is_ajax=False): # pylint: disable=arguments-differ
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def call_view(
self,
mock_is_forum_v2_enabled,
mock_request,
commentable_id,
user,
group_id,
pass_group_id=True,
is_ajax=False
): # pylint: disable=arguments-differ
mock_is_forum_v2_enabled.return_value = False
kwargs = {}
if group_id:
kwargs['group_id'] = group_id
@@ -1120,8 +1250,9 @@ class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdT
**headers
)
def test_group_info_in_html_response(self, mock_request):
def test_group_info_in_html_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
@@ -1129,8 +1260,9 @@ class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdT
)
self._assert_html_response_contains_group_info(response)
def test_group_info_in_ajax_response(self, mock_request):
def test_group_info_in_ajax_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
@@ -1143,16 +1275,38 @@ class ForumFormDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdT
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin): # lint-amnesty, pylint: disable=missing-class-docstring
cs_endpoint = "/active_threads"
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def call_view_for_profiled_user(
self, mock_request, requesting_user, profiled_user, group_id, pass_group_id, is_ajax=False
self,
mock_is_forum_v2_enabled,
mock_request,
requesting_user,
profiled_user,
group_id,
pass_group_id,
is_ajax=False
):
"""
Calls "user_profile" view method on behalf of "requesting_user" to get information about
the user "profiled_user".
"""
mock_is_forum_v2_enabled.return_value = False
kwargs = {}
if group_id:
kwargs['group_id'] = group_id
@@ -1172,13 +1326,23 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
**headers
)
def call_view(self, mock_request, _commentable_id, user, group_id, pass_group_id=True, is_ajax=False): # pylint: disable=arguments-differ
def call_view(
self,
mock_is_forum_v2_enabled,
mock_request,
_commentable_id,
user,
group_id,
pass_group_id=True,
is_ajax=False
): # pylint: disable=arguments-differ
return self.call_view_for_profiled_user(
mock_request, user, user, group_id, pass_group_id=pass_group_id, is_ajax=is_ajax
mock_is_forum_v2_enabled, mock_request, user, user, group_id, pass_group_id=pass_group_id, is_ajax=is_ajax
)
def test_group_info_in_html_response(self, mock_request):
def test_group_info_in_html_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
@@ -1187,8 +1351,9 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
)
self._assert_html_response_contains_group_info(response)
def test_group_info_in_ajax_response(self, mock_request):
def test_group_info_in_ajax_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
@@ -1200,7 +1365,14 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
)
def _test_group_id_passed_to_user_profile(
self, mock_request, expect_group_id_in_request, requesting_user, profiled_user, group_id, pass_group_id
self,
mock_is_forum_v2_enabled,
mock_request,
expect_group_id_in_request,
requesting_user,
profiled_user,
group_id,
pass_group_id
):
"""
Helper method for testing whether or not group_id was passed to the user_profile request.
@@ -1221,10 +1393,11 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
has_course_id = "course_id" in params
if (for_specific_course and has_course_id) or (not for_specific_course and not has_course_id):
return params
pytest.fail("Did not find appropriate user_profile call for 'for_specific_course'=" + for_specific_course)
pytest.fail(f"Did not find appropriate user_profile call for 'for_specific_course'={for_specific_course}")
mock_request.reset_mock()
self.call_view_for_profiled_user(
mock_is_forum_v2_enabled,
mock_request,
requesting_user,
profiled_user,
@@ -1243,7 +1416,7 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
else:
assert 'group_id' not in params_with_course_id
def test_group_id_passed_to_user_profile_student(self, mock_request):
def test_group_id_passed_to_user_profile_student(self, mock_is_forum_v2_enabled, mock_request):
"""
Test that the group id is always included when requesting user profile information for a particular
course if the requester does not have discussion moderation privileges.
@@ -1254,7 +1427,13 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
(non-privileged user).
"""
self._test_group_id_passed_to_user_profile(
mock_request, True, self.student, profiled_user, self.student_cohort.id, pass_group_id
mock_is_forum_v2_enabled,
mock_request,
True,
self.student,
profiled_user,
self.student_cohort.id,
pass_group_id
)
# In all these test cases, the requesting_user is the student (non-privileged user).
@@ -1264,7 +1443,7 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
verify_group_id_always_present(profiled_user=self.moderator, pass_group_id=True)
verify_group_id_always_present(profiled_user=self.moderator, pass_group_id=False)
def test_group_id_user_profile_moderator(self, mock_request):
def test_group_id_user_profile_moderator(self, mock_is_forum_v2_enabled, mock_request):
"""
Test that the group id is only included when a privileged user requests user profile information for a
particular course and user if the group_id is explicitly passed in.
@@ -1274,7 +1453,13 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
Helper method to verify that group_id is present.
"""
self._test_group_id_passed_to_user_profile(
mock_request, True, self.moderator, profiled_user, requested_cohort.id, pass_group_id
mock_is_forum_v2_enabled,
mock_request,
True,
self.moderator,
profiled_user,
requested_cohort.id,
pass_group_id
)
def verify_group_id_not_present(profiled_user, pass_group_id, requested_cohort=self.moderator_cohort):
@@ -1282,7 +1467,13 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
Helper method to verify that group_id is not present.
"""
self._test_group_id_passed_to_user_profile(
mock_request, False, self.moderator, profiled_user, requested_cohort.id, pass_group_id
mock_is_forum_v2_enabled,
mock_request,
False,
self.moderator,
profiled_user,
requested_cohort.id,
pass_group_id
)
# In all these test cases, the requesting_user is the moderator (privileged user).
@@ -1301,10 +1492,28 @@ class UserProfileDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupI
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
class FollowedThreadsDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGroupIdTestMixin): # lint-amnesty, pylint: disable=missing-class-docstring
cs_endpoint = "/subscribed_threads"
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
def setUp(self):
super().setUp()
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
def call_view(
self,
mock_is_forum_v2_enabled,
mock_request,
commentable_id,
user,
group_id,
pass_group_id=True
): # pylint: disable=arguments-differ
mock_is_forum_v2_enabled.return_value = False
kwargs = {}
if group_id:
kwargs['group_id'] = group_id
@@ -1325,8 +1534,9 @@ class FollowedThreadsDiscussionGroupIdTestCase(CohortedTestCase, CohortedTopicGr
user.id
)
def test_group_info_in_ajax_response(self, mock_request):
def test_group_info_in_ajax_response(self, mock_is_forum_v2_enabled, mock_request):
response = self.call_view(
mock_is_forum_v2_enabled,
mock_request,
"cohorted_topic",
self.student,
@@ -1528,6 +1738,22 @@ class CommentsServiceRequestHeadersTestCase(ForumsEnableMixin, UrlResetMixin, Mo
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
)
self.mock_get_course_id_by_comment = patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
username = "foo"
password = "bar"
@@ -1742,6 +1968,20 @@ class SingleThreadUnicodeTestCase(ForumsEnableMixin, SharedModuleStoreTestCase,
with super().setUpClassAndTestData():
cls.course = CourseFactory.create(discussion_topics={'dummy_discussion_id': {'id': 'dummy_discussion_id'}})
def setUp(self):
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def setUpTestData(cls):
super().setUpTestData()
@@ -1858,7 +2098,17 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ForumsEnableMixin
def setUp(self):
# Invoke UrlResetMixin setUp
super().setUp()
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
username = "foo"
password = "bar"
@@ -2195,6 +2445,17 @@ class ThreadViewedEventTestCase(EventTestMixin, ForumsEnableMixin, UrlResetMixin
def setUp(self): # pylint: disable=arguments-differ
super().setUp('lms.djangoapps.discussion.django_comment_client.base.views.tracker')
patcher = mock.patch(
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
return_value=False
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = mock.patch(
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
)
self.mock_get_course_id_by_thread = patcher.start()
self.addCleanup(patcher.stop)
self.course = CourseFactory.create(
teams_configuration=TeamsConfig({
'topics': [{

View File

@@ -1,6 +1,7 @@
"""
Discussions feature toggles
"""
from openedx.core.djangoapps.discussions.config.waffle import WAFFLE_FLAG_NAMESPACE
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
@@ -11,4 +12,6 @@ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2021-11-05
# .. toggle_target_removal_date: 2022-12-05
ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.enable_discussions_mfe', __name__)
ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(
f"{WAFFLE_FLAG_NAMESPACE}.enable_discussions_mfe", __name__
)

View File

@@ -2,6 +2,8 @@
This module contains various configuration settings via
waffle switches for the discussions app.
"""
from django.conf import settings
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = "discussions"
@@ -43,3 +45,31 @@ ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND = CourseWaffleFlag(
ENABLE_NEW_STRUCTURE_DISCUSSIONS = CourseWaffleFlag(
f"{WAFFLE_FLAG_NAMESPACE}.enable_new_structure_discussions", __name__
)
# .. toggle_name: discussions.enable_forum_v2
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to use the forum v2 instead of v1(cs_comment_service)
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2024-9-26
# .. toggle_target_removal_date: 2025-12-05
ENABLE_FORUM_V2 = CourseWaffleFlag(f"{WAFFLE_FLAG_NAMESPACE}.enable_forum_v2", __name__)
def is_forum_v2_enabled(course_id):
"""
Returns whether forum V2 is enabled on the course. This is a 2-step check:
1. Check value of settings.DISABLE_FORUM_V2: if it exists and is true, this setting overrides any course flag.
2. Else, check the value of the corresponding course waffle flag.
"""
if is_forum_v2_disabled_globally():
return False
return ENABLE_FORUM_V2.is_enabled(course_id)
def is_forum_v2_disabled_globally() -> bool:
"""
Return True if DISABLE_FORUM_V2 is defined and true-ish.
"""
return getattr(settings, "DISABLE_FORUM_V2", False)

View File

@@ -4,7 +4,9 @@ from bs4 import BeautifulSoup
from openedx.core.djangoapps.django_comment_common.comment_client import models, settings
from .thread import Thread, _url_for_flag_abuse_thread, _url_for_unflag_abuse_thread
from .utils import CommentClientRequestError, perform_request
from .utils import CommentClientRequestError, get_course_key, perform_request
from forum import api as forum_api
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
class Comment(models.Model):
@@ -68,14 +70,21 @@ class Comment(models.Model):
url = _url_for_flag_abuse_comment(voteable.id)
else:
raise CommentClientRequestError("Can only flag/unflag threads or comments")
params = {'user_id': user.id}
response = perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='comment.abuse.flagged'
)
course_key = get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
if voteable.type == 'thread':
response = forum_api.update_thread_flag(voteable.id, "flag", user.id, str(course_key))
else:
response = forum_api.update_comment_flag(voteable.id, "flag", user.id, str(course_key))
else:
params = {'user_id': user.id}
response = perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='comment.abuse.flagged'
)
voteable._update_from_response(response)
def unFlagAbuse(self, user, voteable, removeAll):
@@ -85,18 +94,37 @@ class Comment(models.Model):
url = _url_for_unflag_abuse_comment(voteable.id)
else:
raise CommentClientRequestError("Can flag/unflag for threads or comments")
params = {'user_id': user.id}
course_key = get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
if voteable.type == "thread":
response = forum_api.update_thread_flag(
thread_id=voteable.id,
action="unflag",
user_id=user.id,
update_all=bool(removeAll),
course_id=str(course_key)
)
else:
response = forum_api.update_comment_flag(
comment_id=voteable.id,
action="unflag",
user_id=user.id,
update_all=bool(removeAll),
course_id=str(course_key)
)
else:
params = {'user_id': user.id}
if removeAll:
params['all'] = True
if removeAll:
params['all'] = True
response = perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='comment.abuse.unflagged'
)
response = perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='comment.abuse.unflagged'
)
voteable._update_from_response(response)
@property

View File

@@ -7,8 +7,10 @@ from typing import Dict, Optional
from edx_django_utils.monitoring import function_trace
from opaque_keys.edx.keys import CourseKey
from forum import api as forum_api
from openedx.core.djangoapps.django_comment_common.comment_client import settings
from openedx.core.djangoapps.django_comment_common.comment_client.utils import perform_request
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
def get_course_commentable_counts(course_key: CourseKey) -> Dict[str, Dict[str, int]]:
@@ -29,17 +31,20 @@ def get_course_commentable_counts(course_key: CourseKey) -> Dict[str, Dict[str,
}
"""
url = f"{settings.PREFIX}/commentables/{course_key}/counts"
response = perform_request(
'get',
url,
metric_tags=[
f"course_key:{course_key}",
"function:get_course_commentable_counts",
],
metric_action='commentable_stats.retrieve',
)
return response
if is_forum_v2_enabled(course_key):
commentable_stats = forum_api.get_commentables_stats(str(course_key))
else:
url = f"{settings.PREFIX}/commentables/{course_key}/counts"
commentable_stats = perform_request(
'get',
url,
metric_tags=[
f"course_key:{course_key}",
"function:get_course_commentable_counts",
],
metric_action='commentable_stats.retrieve',
)
return commentable_stats
@function_trace("get_course_user_stats")
@@ -76,17 +81,21 @@ def get_course_user_stats(course_key: CourseKey, params: Optional[Dict] = None)
"""
if params is None:
params = {}
url = f"{settings.PREFIX}/users/{course_key}/stats"
return perform_request(
'get',
url,
params,
metric_action='user.course_stats',
metric_tags=[
f"course_key:{course_key}",
"function:get_course_user_stats",
],
)
if is_forum_v2_enabled(course_key):
course_stats = forum_api.get_user_course_stats(str(course_key), **params)
else:
url = f"{settings.PREFIX}/users/{course_key}/stats"
course_stats = perform_request(
'get',
url,
params,
metric_action='user.course_stats',
metric_tags=[
f"course_key:{course_key}",
"function:get_course_user_stats",
],
)
return course_stats
@function_trace("update_course_users_stats")
@@ -100,13 +109,17 @@ def update_course_users_stats(course_key: CourseKey) -> Dict:
Returns:
dict: data returned by API. Contains count of users updated.
"""
url = f"{settings.PREFIX}/users/{course_key}/update_stats"
return perform_request(
'post',
url,
metric_action='user.update_course_stats',
metric_tags=[
f"course_key:{course_key}",
"function:update_course_users_stats",
],
)
if is_forum_v2_enabled(course_key):
course_stats = forum_api.update_users_in_course(str(course_key))
else:
url = f"{settings.PREFIX}/users/{course_key}/update_stats"
course_stats = perform_request(
'post',
url,
metric_action='user.update_course_stats',
metric_tags=[
f"course_key:{course_key}",
"function:update_course_users_stats",
],
)
return course_stats

View File

@@ -2,8 +2,11 @@
import logging
import typing as t
from .utils import CommentClientRequestError, extract, perform_request
from .utils import CommentClientRequestError, extract, perform_request, get_course_key
from forum import api as forum_api
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled, is_forum_v2_disabled_globally
log = logging.getLogger(__name__)
@@ -69,14 +72,26 @@ class Model:
return self
def _retrieve(self, *args, **kwargs):
url = self.url(action='get', params=self.attributes)
response = perform_request(
'get',
url,
self.default_retrieve_params,
metric_tags=self._metric_tags,
metric_action='model.retrieve'
)
course_id = self.attributes.get("course_id") or kwargs.get("course_id")
if course_id:
use_forumv2 = is_forum_v2_enabled(course_id)
else:
use_forumv2, course_id = is_forum_v2_enabled_for_comment(self.id)
response = None
if use_forumv2:
if self.type == "comment":
response = forum_api.get_parent_comment(comment_id=self.attributes["id"], course_id=course_id)
if response is None:
raise CommentClientRequestError("Forum v2 API call is missing")
else:
url = self.url(action='get', params=self.attributes)
response = perform_request(
'get',
url,
self.default_retrieve_params,
metric_tags=self._metric_tags,
metric_action='model.retrieve'
)
self._update_from_response(response)
@property
@@ -151,33 +166,27 @@ class Model:
"""
self.before_save(self)
if self.id: # if we have id already, treat this as an update
request_params = self.updatable_attributes()
if params:
request_params.update(params)
url = self.url(action='put', params=self.attributes)
response = perform_request(
'put',
url,
request_params,
metric_tags=self._metric_tags,
metric_action='model.update'
)
else: # otherwise, treat this as an insert
url = self.url(action='post', params=self.attributes)
response = perform_request(
'post',
url,
self.initializable_attributes(),
metric_tags=self._metric_tags,
metric_action='model.insert'
)
response = self.handle_update(params)
else: # otherwise, treat this as an insert
response = self.handle_create(params)
self.retrieved = True
self._update_from_response(response)
self.after_save(self)
def delete(self):
url = self.url(action='delete', params=self.attributes)
response = perform_request('delete', url, metric_tags=self._metric_tags, metric_action='model.delete')
course_key = get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
response = None
if self.type == "comment":
response = forum_api.delete_comment(comment_id=self.attributes["id"], course_id=str(course_key))
elif self.type == "thread":
response = forum_api.delete_thread(thread_id=self.attributes["id"], course_id=str(course_key))
if response is None:
raise CommentClientRequestError("Forum v2 API call is missing")
else:
url = self.url(action='delete', params=self.attributes)
response = perform_request('delete', url, metric_tags=self._metric_tags, metric_action='model.delete')
self.retrieved = True
self._update_from_response(response)
@@ -208,3 +217,176 @@ class Model:
raise CommentClientRequestError(f"Cannot perform action {action} without id") # lint-amnesty, pylint: disable=raise-missing-from
else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now
return cls.url_without_id()
def handle_update(self, params=None):
request_params = self.updatable_attributes()
if params:
request_params.update(params)
course_id = self.attributes.get("course_id") or request_params.get("course_id")
course_key = get_course_key(course_id)
if is_forum_v2_enabled(course_key):
response = None
if self.type == "comment":
response = self.handle_update_comment(request_params, str(course_key))
elif self.type == "thread":
response = self.handle_update_thread(request_params, str(course_key))
elif self.type == "user":
response = self.handle_update_user(request_params, str(course_key))
if response is None:
raise CommentClientRequestError("Forum v2 API call is missing")
else:
response = self.perform_http_put_request(request_params)
return response
def handle_update_user(self, request_params, course_id):
try:
username = request_params["username"]
external_id = str(request_params["external_id"])
except KeyError as e:
raise e
response = forum_api.update_user(
external_id,
username=username,
course_id=course_id,
)
return response
def handle_update_comment(self, request_params, course_id):
request_data = {
"comment_id": self.attributes["id"],
"body": request_params.get("body"),
"course_id": request_params.get("course_id"),
"user_id": request_params.get("user_id"),
"anonymous": request_params.get("anonymous"),
"anonymous_to_peers": request_params.get("anonymous_to_peers"),
"endorsed": request_params.get("endorsed"),
"closed": request_params.get("closed"),
"editing_user_id": request_params.get("editing_user_id"),
"edit_reason_code": request_params.get("edit_reason_code"),
"endorsement_user_id": request_params.get("endorsement_user_id"),
"course_key": course_id
}
request_data = {k: v for k, v in request_data.items() if v is not None}
response = forum_api.update_comment(**request_data)
return response
def handle_update_thread(self, request_params, course_id):
request_data = {
"thread_id": self.attributes["id"],
"title": request_params.get("title"),
"body": request_params.get("body"),
"course_id": request_params.get("course_id"),
"anonymous": request_params.get("anonymous"),
"anonymous_to_peers": request_params.get("anonymous_to_peers"),
"closed": request_params.get("closed"),
"commentable_id": request_params.get("commentable_id"),
"user_id": request_params.get("user_id"),
"editing_user_id": request_params.get("editing_user_id"),
"pinned": request_params.get("pinned"),
"thread_type": request_params.get("thread_type"),
"edit_reason_code": request_params.get("edit_reason_code"),
"close_reason_code": request_params.get("close_reason_code"),
"closing_user_id": request_params.get("closing_user_id"),
"endorsed": request_params.get("endorsed"),
"course_key": course_id
}
request_data = {k: v for k, v in request_data.items() if v is not None}
response = forum_api.update_thread(**request_data)
return response
def perform_http_put_request(self, request_params):
url = self.url(action="put", params=self.attributes)
response = perform_request(
"put",
url,
request_params,
metric_tags=self._metric_tags,
metric_action="model.update",
)
return response
def perform_http_post_request(self):
url = self.url(action="post", params=self.attributes)
response = perform_request(
"post",
url,
self.initializable_attributes(),
metric_tags=self._metric_tags,
metric_action="model.insert",
)
return response
def handle_create(self, params=None):
course_id = self.attributes.get("course_id") or params.get("course_id")
course_key = get_course_key(course_id)
if is_forum_v2_enabled(course_key):
response = None
if self.type == "comment":
response = self.handle_create_comment(str(course_key))
elif self.type == "thread":
response = self.handle_create_thread(str(course_key))
if response is None:
raise CommentClientRequestError("Forum v2 API call is missing")
else:
response = self.perform_http_post_request()
return response
def handle_create_comment(self, course_id):
request_data = self.initializable_attributes()
body = request_data["body"]
user_id = request_data["user_id"]
course_id = course_id or str(request_data["course_id"])
if parent_id := self.attributes.get("parent_id"):
response = forum_api.create_child_comment(
parent_id,
body,
user_id,
course_id,
request_data.get("anonymous", False),
request_data.get("anonymous_to_peers", False),
)
else:
response = forum_api.create_parent_comment(
self.attributes["thread_id"],
body,
user_id,
course_id,
request_data.get("anonymous", False),
request_data.get("anonymous_to_peers", False),
)
return response
def handle_create_thread(self, course_id):
request_data = self.initializable_attributes()
response = forum_api.create_thread(
title=request_data["title"],
body=request_data["body"],
course_id=course_id or str(request_data["course_id"]),
user_id=str(request_data["user_id"]),
anonymous=request_data.get("anonymous", False),
anonymous_to_peers=request_data.get("anonymous_to_peers", False),
commentable_id=request_data.get("commentable_id", "course"),
thread_type=request_data.get("thread_type", "discussion"),
group_id=request_data.get("group_id", None),
context=request_data.get("context", None),
)
return response
def is_forum_v2_enabled_for_comment(comment_id: str) -> tuple[bool, t.Optional[str]]:
"""
Figure out whether we use forum v2 for a given comment.
See is_forum_v2_enabled_for_thread.
Return:
enabled (bool)
course_id (str or None)
"""
if is_forum_v2_disabled_globally():
return False, None
course_id = forum_api.get_course_id_by_comment(comment_id)
course_key = get_course_key(course_id)
return is_forum_v2_enabled(course_key), course_id

View File

@@ -4,6 +4,8 @@ Subscription model is used to get users who are subscribed to the main thread/po
import logging
from . import models, settings, utils
from forum import api as forum_api
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
log = logging.getLogger(__name__)
@@ -21,7 +23,7 @@ class Subscription(models.Model):
base_url = f"{settings.PREFIX}/threads"
@classmethod
def fetch(cls, thread_id, query_params):
def fetch(cls, thread_id, course_id, query_params):
"""
Fetches the subscriptions for a given thread_id
"""
@@ -33,14 +35,23 @@ class Subscription(models.Model):
params.update(
utils.strip_blank(utils.strip_none(query_params))
)
response = utils.perform_request(
'get',
cls.url(action='get', params=params) + "/subscriptions",
params,
metric_tags=[],
metric_action='subscription.get',
paged_results=True
)
course_key = utils.get_course_key(course_id)
if is_forum_v2_enabled(course_key):
response = forum_api.get_thread_subscriptions(
thread_id=thread_id,
page=params["page"],
per_page=params["per_page"],
course_id=str(course_key)
)
else:
response = utils.perform_request(
'get',
cls.url(action='get', params=params) + "/subscriptions",
params,
metric_tags=[],
metric_action='subscription.get',
paged_results=True
)
return utils.SubscriptionsPaginatedResult(
collection=response.get('collection', []),
page=response.get('page', 1),

View File

@@ -2,10 +2,13 @@
import logging
import typing as t
from eventtracking import tracker
from . import models, settings, utils
from forum import api as forum_api
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled, is_forum_v2_disabled_globally
log = logging.getLogger(__name__)
@@ -59,14 +62,35 @@ class Thread(models.Model):
url = cls.url(action='get_all', params=utils.extract(params, 'commentable_id'))
if params.get('commentable_id'):
del params['commentable_id']
response = utils.perform_request(
'get',
url,
params,
metric_tags=['course_id:{}'.format(query_params['course_id'])],
metric_action='thread.search',
paged_results=True
)
if is_forum_v2_enabled(utils.get_course_key(query_params['course_id'])):
if query_params.get('text'):
search_params = utils.strip_none(params)
if user_id := search_params.get('user_id'):
search_params['user_id'] = str(user_id)
if group_ids := search_params.get('group_ids'):
search_params['group_ids'] = [int(group_id) for group_id in group_ids.split(',')]
elif group_id := search_params.get('group_id'):
search_params['group_ids'] = [int(group_id)]
search_params.pop('group_id', None)
if commentable_ids := search_params.get('commentable_ids'):
search_params['commentable_ids'] = commentable_ids.split(',')
elif commentable_id := search_params.get('commentable_id'):
search_params['commentable_ids'] = [commentable_id]
search_params.pop('commentable_id', None)
response = forum_api.search_threads(**search_params)
else:
response = forum_api.get_user_threads(**params)
else:
response = utils.perform_request(
'get',
url,
params,
metric_tags=['course_id:{}'.format(query_params['course_id'])],
metric_action='thread.search',
paged_results=True
)
if query_params.get('text'):
search_query = query_params['text']
course_id = query_params['course_id']
@@ -148,14 +172,27 @@ class Thread(models.Model):
'merge_question_type_responses': kwargs.get('merge_question_type_responses', False)
}
request_params = utils.strip_none(request_params)
response = utils.perform_request(
'get',
url,
request_params,
metric_action='model.retrieve',
metric_tags=self._metric_tags
)
course_id = kwargs.get("course_id")
if course_id:
use_forumv2 = is_forum_v2_enabled(course_id)
else:
use_forumv2, course_id = is_forum_v2_enabled_for_thread(self.id)
if use_forumv2:
if user_id := request_params.get('user_id'):
request_params['user_id'] = str(user_id)
response = forum_api.get_thread(
thread_id=self.id,
params=request_params,
course_id=course_id,
)
else:
response = utils.perform_request(
'get',
url,
request_params,
metric_action='model.retrieve',
metric_tags=self._metric_tags
)
self._update_from_response(response)
def flagAbuse(self, user, voteable):
@@ -163,14 +200,18 @@ class Thread(models.Model):
url = _url_for_flag_abuse_thread(voteable.id)
else:
raise utils.CommentClientRequestError("Can only flag/unflag threads or comments")
params = {'user_id': user.id}
response = utils.perform_request(
'put',
url,
params,
metric_action='thread.abuse.flagged',
metric_tags=self._metric_tags
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
response = forum_api.update_thread_flag(voteable.id, "flag", user.id, str(course_key))
else:
params = {'user_id': user.id}
response = utils.perform_request(
'put',
url,
params,
metric_action='thread.abuse.flagged',
metric_tags=self._metric_tags
)
voteable._update_from_response(response)
def unFlagAbuse(self, user, voteable, removeAll):
@@ -178,42 +219,68 @@ class Thread(models.Model):
url = _url_for_unflag_abuse_thread(voteable.id)
else:
raise utils.CommentClientRequestError("Can only flag/unflag for threads or comments")
params = {'user_id': user.id}
#if you're an admin, when you unflag, remove ALL flags
if removeAll:
params['all'] = True
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
response = forum_api.update_thread_flag(
thread_id=voteable.id,
action="unflag",
user_id=user.id,
update_all=bool(removeAll),
course_id=str(course_key)
)
else:
params = {'user_id': user.id}
#if you're an admin, when you unflag, remove ALL flags
if removeAll:
params['all'] = True
response = utils.perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='thread.abuse.unflagged'
)
response = utils.perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='thread.abuse.unflagged'
)
voteable._update_from_response(response)
def pin(self, user, thread_id):
url = _url_for_pin_thread(thread_id)
params = {'user_id': user.id}
response = utils.perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='thread.pin'
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
response = forum_api.pin_thread(
user_id=user.id,
thread_id=thread_id,
course_id=str(course_key)
)
else:
url = _url_for_pin_thread(thread_id)
params = {'user_id': user.id}
response = utils.perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='thread.pin'
)
self._update_from_response(response)
def un_pin(self, user, thread_id):
url = _url_for_un_pin_thread(thread_id)
params = {'user_id': user.id}
response = utils.perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='thread.unpin'
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
response = forum_api.unpin_thread(
user_id=user.id,
thread_id=thread_id,
course_id=str(course_key)
)
else:
url = _url_for_un_pin_thread(thread_id)
params = {'user_id': user.id}
response = utils.perform_request(
'put',
url,
params,
metric_tags=self._metric_tags,
metric_action='thread.unpin'
)
self._update_from_response(response)
@@ -231,3 +298,28 @@ def _url_for_pin_thread(thread_id):
def _url_for_un_pin_thread(thread_id):
return f"{settings.PREFIX}/threads/{thread_id}/unpin"
def is_forum_v2_enabled_for_thread(thread_id: str) -> tuple[bool, t.Optional[str]]:
"""
Figure out whether we use forum v2 for a given thread.
This is a complex affair... First, we check the value of the DISABLE_FORUM_V2
setting, which overrides everything. If this setting does not exist, then we need to
find the course ID that corresponds to the thread ID. Then, we return the value of
the course waffle flag for this course ID.
Note that to fetch the course ID associated to a thread ID, we need to connect both
to mongodb and mysql. As a consequence, when forum v2 needs adequate connection
strings for both backends.
Return:
enabled (bool)
course_id (str or None)
"""
if is_forum_v2_disabled_globally():
return False, None
course_id = forum_api.get_course_id_by_thread(thread_id)
course_key = utils.get_course_key(course_id)
return is_forum_v2_enabled(course_key), course_id

View File

@@ -1,8 +1,10 @@
# pylint: disable=missing-docstring,protected-access
""" User model wrapper for comment service"""
from . import models, settings, utils
from forum import api as forum_api
from forum.utils import ForumV2RequestError, str_to_bool
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
class User(models.Model):
@@ -34,34 +36,55 @@ class User(models.Model):
"""
Calls cs_comments_service to mark thread as read for the user
"""
params = {'source_type': source.type, 'source_id': source.id}
utils.perform_request(
'post',
_url_for_read(self.id),
params,
metric_action='user.read',
metric_tags=self._metric_tags + [f'target.type:{source.type}'],
)
course_id = self.attributes.get("course_id")
course_key = utils.get_course_key(course_id)
if is_forum_v2_enabled(course_key):
forum_api.mark_thread_as_read(self.id, source.id, course_id=str(course_id))
else:
params = {'source_type': source.type, 'source_id': source.id}
utils.perform_request(
'post',
_url_for_read(self.id),
params,
metric_action='user.read',
metric_tags=self._metric_tags + [f'target.type:{source.type}'],
)
def follow(self, source):
params = {'source_type': source.type, 'source_id': source.id}
utils.perform_request(
'post',
_url_for_subscription(self.id),
params,
metric_action='user.follow',
metric_tags=self._metric_tags + [f'target.type:{source.type}'],
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
forum_api.create_subscription(
user_id=self.id,
source_id=source.id,
course_id=str(course_key)
)
else:
params = {'source_type': source.type, 'source_id': source.id}
utils.perform_request(
'post',
_url_for_subscription(self.id),
params,
metric_action='user.follow',
metric_tags=self._metric_tags + [f'target.type:{source.type}'],
)
def unfollow(self, source):
params = {'source_type': source.type, 'source_id': source.id}
utils.perform_request(
'delete',
_url_for_subscription(self.id),
params,
metric_action='user.unfollow',
metric_tags=self._metric_tags + [f'target.type:{source.type}'],
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
forum_api.delete_subscription(
user_id=self.id,
source_id=source.id,
course_id=str(course_key)
)
else:
params = {'source_type': source.type, 'source_id': source.id}
utils.perform_request(
'delete',
_url_for_subscription(self.id),
params,
metric_action='user.unfollow',
metric_tags=self._metric_tags + [f'target.type:{source.type}'],
)
def vote(self, voteable, value):
if voteable.type == 'thread':
@@ -70,14 +93,31 @@ class User(models.Model):
url = _url_for_vote_comment(voteable.id)
else:
raise utils.CommentClientRequestError("Can only vote / unvote for threads or comments")
params = {'user_id': self.id, 'value': value}
response = utils.perform_request(
'put',
url,
params,
metric_action='user.vote',
metric_tags=self._metric_tags + [f'target.type:{voteable.type}'],
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
if voteable.type == 'thread':
response = forum_api.update_thread_votes(
thread_id=voteable.id,
user_id=self.id,
value=value,
course_id=str(course_key)
)
else:
response = forum_api.update_comment_votes(
comment_id=voteable.id,
user_id=self.id,
value=value,
course_id=str(course_key)
)
else:
params = {'user_id': self.id, 'value': value}
response = utils.perform_request(
'put',
url,
params,
metric_action='user.vote',
metric_tags=self._metric_tags + [f'target.type:{voteable.type}'],
)
voteable._update_from_response(response)
def unvote(self, voteable):
@@ -87,14 +127,29 @@ class User(models.Model):
url = _url_for_vote_comment(voteable.id)
else:
raise utils.CommentClientRequestError("Can only vote / unvote for threads or comments")
params = {'user_id': self.id}
response = utils.perform_request(
'delete',
url,
params,
metric_action='user.unvote',
metric_tags=self._metric_tags + [f'target.type:{voteable.type}'],
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
if voteable.type == 'thread':
response = forum_api.delete_thread_vote(
thread_id=voteable.id,
user_id=self.id,
course_id=str(course_key)
)
else:
response = forum_api.delete_comment_vote(
comment_id=voteable.id,
user_id=self.id,
course_id=str(course_key)
)
else:
params = {'user_id': self.id}
response = utils.perform_request(
'delete',
url,
params,
metric_action='user.unvote',
metric_tags=self._metric_tags + [f'target.type:{voteable.type}'],
)
voteable._update_from_response(response)
def active_threads(self, query_params=None):
@@ -105,14 +160,28 @@ class User(models.Model):
url = _url_for_user_active_threads(self.id)
params = {'course_id': str(self.course_id)}
params.update(query_params)
response = utils.perform_request(
'get',
url,
params,
metric_action='user.active_threads',
metric_tags=self._metric_tags,
paged_results=True,
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
if user_id := params.get("user_id"):
params["user_id"] = str(user_id)
if page := params.get("page"):
params["page"] = int(page)
if per_page := params.get("per_page"):
params["per_page"] = int(per_page)
if count_flagged := params.get("count_flagged", False):
params["count_flagged"] = str_to_bool(count_flagged)
if not params.get("course_id"):
params["course_id"] = str(course_key)
response = forum_api.get_user_active_threads(**params)
else:
response = utils.perform_request(
'get',
url,
params,
metric_action='user.active_threads',
metric_tags=self._metric_tags,
paged_results=True,
)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def subscribed_threads(self, query_params=None):
@@ -125,14 +194,28 @@ class User(models.Model):
url = _url_for_user_subscribed_threads(self.id)
params = {'course_id': str(self.course_id)}
params.update(query_params)
response = utils.perform_request(
'get',
url,
params,
metric_action='user.subscribed_threads',
metric_tags=self._metric_tags,
paged_results=True
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
if user_id := params.get("user_id"):
params["user_id"] = str(user_id)
if page := params.get("page"):
params["page"] = int(page)
if per_page := params.get("per_page"):
params["per_page"] = int(per_page)
if count_flagged := params.get("count_flagged", False):
params["count_flagged"] = str_to_bool(count_flagged)
if not params.get("course_id"):
params["course_id"] = str(course_key)
response = forum_api.get_user_threads(**params)
else:
response = utils.perform_request(
'get',
url,
params,
metric_action='user.subscribed_threads',
metric_tags=self._metric_tags,
paged_results=True
)
return utils.CommentClientPaginatedResult(
collection=response.get('collection', []),
page=response.get('page', 1),
@@ -144,23 +227,39 @@ class User(models.Model):
url = self.url(action='get', params=self.attributes)
retrieve_params = self.default_retrieve_params.copy()
retrieve_params.update(kwargs)
if self.attributes.get('course_id'):
retrieve_params['course_id'] = str(self.course_id)
if self.attributes.get('group_id'):
retrieve_params['group_id'] = self.group_id
try:
response = utils.perform_request(
'get',
url,
retrieve_params,
metric_action='model.retrieve',
metric_tags=self._metric_tags,
)
except utils.CommentClientRequestError as e:
if e.status_code == 404:
# attempt to gracefully recover from a previous failure
# to sync this user to the comments service.
self.save()
# course key -> id conversation
course_id = retrieve_params.get('course_id')
if course_id:
course_id = str(course_id)
retrieve_params['course_id'] = course_id
course_key = utils.get_course_key(course_id)
if is_forum_v2_enabled(course_key):
group_ids = [retrieve_params['group_id']] if 'group_id' in retrieve_params else []
is_complete = retrieve_params['complete']
try:
response = forum_api.get_user(
self.attributes["id"],
group_ids=group_ids,
course_id=course_id,
complete=is_complete
)
except ForumV2RequestError as e:
self.save({"course_id": course_id})
response = forum_api.get_user(
self.attributes["id"],
group_ids=group_ids,
course_id=course_id,
complete=is_complete
)
else:
try:
response = utils.perform_request(
'get',
url,
@@ -168,33 +267,52 @@ class User(models.Model):
metric_action='model.retrieve',
metric_tags=self._metric_tags,
)
else:
raise
except utils.CommentClientRequestError as e:
if e.status_code == 404:
# attempt to gracefully recover from a previous failure
# to sync this user to the comments service.
self.save()
response = utils.perform_request(
'get',
url,
retrieve_params,
metric_action='model.retrieve',
metric_tags=self._metric_tags,
)
else:
raise
self._update_from_response(response)
def retire(self, retired_username):
url = _url_for_retire(self.id)
params = {'retired_username': retired_username}
utils.perform_request(
'post',
url,
params,
raw=True,
metric_action='user.retire',
metric_tags=self._metric_tags
)
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
forum_api.retire_user(user_id=self.id, retired_username=retired_username, course_id=str(course_key))
else:
url = _url_for_retire(self.id)
params = {'retired_username': retired_username}
utils.perform_request(
'post',
url,
params,
raw=True,
metric_action='user.retire',
metric_tags=self._metric_tags
)
def replace_username(self, new_username):
url = _url_for_username_replacement(self.id)
params = {"new_username": new_username}
course_key = utils.get_course_key(self.attributes.get("course_id"))
if is_forum_v2_enabled(course_key):
forum_api.update_username(user_id=self.id, new_username=new_username, course_id=str(course_key))
else:
url = _url_for_username_replacement(self.id)
params = {"new_username": new_username}
utils.perform_request(
'post',
url,
params,
raw=True,
)
utils.perform_request(
'post',
url,
params,
raw=True,
)
def _url_for_vote_comment(comment_id):

View File

@@ -7,6 +7,7 @@ from uuid import uuid4
import requests
from django.utils.translation import get_language
from opaque_keys.edx.keys import CourseKey
from .settings import SERVICE_HOST as COMMENTS_SERVICE
@@ -167,3 +168,19 @@ def check_forum_heartbeat():
return 'forum', False, res.get('check', 'Forum heartbeat failed')
except Exception as fail:
return 'forum', False, str(fail)
def get_course_key(course_id: CourseKey | str | None) -> CourseKey | None:
"""
Returns a CourseKey if the provided course_id is a valid string representation of a CourseKey.
If course_id is None or already a CourseKey object, it returns the course_id as is.
Args:
course_id (CourseKey | str | None): The course ID to be converted.
Returns:
CourseKey | None: The corresponding CourseKey object or None if the input is None.
Raises:
KeyError: If course_id is not a valid string representation of a CourseKey.
"""
if course_id and isinstance(course_id, str):
course_id = CourseKey.from_string(course_id)
return course_id

View File

@@ -57,7 +57,9 @@ backoff==1.10.0
bcrypt==4.2.1
# via paramiko
beautifulsoup4==4.12.3
# via pynliner
# via
# openedx-forum
# pynliner
billiard==4.2.1
# via celery
bleach[css]==6.2.0
@@ -234,6 +236,7 @@ django==4.2.17
# openedx-django-wiki
# openedx-events
# openedx-filters
# openedx-forum
# openedx-learning
# ora2
# social-auth-app-django
@@ -383,6 +386,7 @@ djangorestframework==3.14.0
# edx-organizations
# edx-proctoring
# edx-submissions
# openedx-forum
# openedx-learning
# ora2
# super-csv
@@ -516,7 +520,9 @@ edx-rest-api-client==6.0.0
# edx-enterprise
# edx-proctoring
edx-search==4.1.1
# via -r requirements/edx/kernel.in
# via
# -r requirements/edx/kernel.in
# openedx-forum
edx-sga==0.25.0
# via -r requirements/edx/bundled.in
edx-submissions==3.8.3
@@ -549,6 +555,7 @@ elasticsearch==7.9.1
# via
# -c requirements/edx/../common_constraints.txt
# edx-search
# openedx-forum
enmerkar==0.7.1
# via enmerkar-underscore
enmerkar-underscore==2.3.1
@@ -774,7 +781,9 @@ multidict==6.1.0
# aiohttp
# yarl
mysqlclient==2.2.6
# via -r requirements/edx/kernel.in
# via
# -r requirements/edx/kernel.in
# openedx-forum
newrelic==10.3.1
# via edx-django-utils
nh3==0.2.19
@@ -804,7 +813,9 @@ openai==0.28.1
# -c requirements/edx/../constraints.txt
# edx-enterprise
openedx-atlas==0.6.2
# via -r requirements/edx/kernel.in
# via
# -r requirements/edx/kernel.in
# openedx-forum
openedx-calc==4.0.1
# via -r requirements/edx/kernel.in
openedx-django-pyfs==3.7.0
@@ -830,6 +841,8 @@ openedx-filters==1.11.0
# -r requirements/edx/kernel.in
# lti-consumer-xblock
# ora2
openedx-forum==0.1.5
# via -r requirements/edx/kernel.in
openedx-learning==0.18.1
# via
# -c requirements/edx/../constraints.txt
@@ -966,6 +979,7 @@ pymongo==4.4.0
# edx-opaque-keys
# event-tracking
# mongoengine
# openedx-forum
# openedx-mongodbproxy
pynacl==1.5.0
# via
@@ -1075,6 +1089,7 @@ requests==2.32.3
# mailsnake
# meilisearch
# openai
# openedx-forum
# optimizely-sdk
# pyjwkest
# pylti1p3

View File

@@ -122,6 +122,7 @@ beautifulsoup4==4.12.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-forum
# pydata-sphinx-theme
# pynliner
billiard==4.2.1
@@ -406,6 +407,7 @@ django==4.2.17
# openedx-django-wiki
# openedx-events
# openedx-filters
# openedx-forum
# openedx-learning
# ora2
# social-auth-app-django
@@ -619,6 +621,7 @@ djangorestframework==3.14.0
# edx-organizations
# edx-proctoring
# edx-submissions
# openedx-forum
# openedx-learning
# ora2
# super-csv
@@ -815,6 +818,7 @@ edx-search==4.1.1
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-forum
edx-sga==0.25.0
# via
# -r requirements/edx/doc.txt
@@ -861,6 +865,7 @@ elasticsearch==7.9.1
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# edx-search
# openedx-forum
enmerkar==0.7.1
# via
# -r requirements/edx/doc.txt
@@ -1304,6 +1309,7 @@ mysqlclient==2.2.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-forum
newrelic==10.3.1
# via
# -r requirements/edx/doc.txt
@@ -1354,6 +1360,7 @@ openedx-atlas==0.6.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-forum
openedx-calc==4.0.1
# via
# -r requirements/edx/doc.txt
@@ -1389,6 +1396,10 @@ openedx-filters==1.11.0
# -r requirements/edx/testing.txt
# lti-consumer-xblock
# ora2
openedx-forum==0.1.5
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
openedx-learning==0.18.1
# via
# -c requirements/edx/../constraints.txt
@@ -1659,6 +1670,7 @@ pymongo==4.4.0
# edx-opaque-keys
# event-tracking
# mongoengine
# openedx-forum
# openedx-mongodbproxy
pynacl==1.5.0
# via
@@ -1855,6 +1867,7 @@ requests==2.32.3
# mailsnake
# meilisearch
# openai
# openedx-forum
# optimizely-sdk
# pact-python
# pyjwkest

View File

@@ -89,6 +89,7 @@ bcrypt==4.2.1
beautifulsoup4==4.12.3
# via
# -r requirements/edx/base.txt
# openedx-forum
# pydata-sphinx-theme
# pynliner
billiard==4.2.1
@@ -292,6 +293,7 @@ django==4.2.17
# openedx-django-wiki
# openedx-events
# openedx-filters
# openedx-forum
# openedx-learning
# ora2
# social-auth-app-django
@@ -457,6 +459,7 @@ djangorestframework==3.14.0
# edx-organizations
# edx-proctoring
# edx-submissions
# openedx-forum
# openedx-learning
# ora2
# super-csv
@@ -601,7 +604,9 @@ edx-rest-api-client==6.0.0
# edx-enterprise
# edx-proctoring
edx-search==4.1.1
# via -r requirements/edx/base.txt
# via
# -r requirements/edx/base.txt
# openedx-forum
edx-sga==0.25.0
# via -r requirements/edx/base.txt
edx-submissions==3.8.3
@@ -637,6 +642,7 @@ elasticsearch==7.9.1
# -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# edx-search
# openedx-forum
enmerkar==0.7.1
# via
# -r requirements/edx/base.txt
@@ -937,7 +943,9 @@ multidict==6.1.0
# aiohttp
# yarl
mysqlclient==2.2.6
# via -r requirements/edx/base.txt
# via
# -r requirements/edx/base.txt
# openedx-forum
newrelic==10.3.1
# via
# -r requirements/edx/base.txt
@@ -973,7 +981,9 @@ openai==0.28.1
# -r requirements/edx/base.txt
# edx-enterprise
openedx-atlas==0.6.2
# via -r requirements/edx/base.txt
# via
# -r requirements/edx/base.txt
# openedx-forum
openedx-calc==4.0.1
# via -r requirements/edx/base.txt
openedx-django-pyfs==3.7.0
@@ -1000,6 +1010,8 @@ openedx-filters==1.11.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
openedx-forum==0.1.5
# via -r requirements/edx/base.txt
openedx-learning==0.18.1
# via
# -c requirements/edx/../constraints.txt
@@ -1171,6 +1183,7 @@ pymongo==4.4.0
# edx-opaque-keys
# event-tracking
# mongoengine
# openedx-forum
# openedx-mongodbproxy
pynacl==1.5.0
# via
@@ -1296,6 +1309,7 @@ requests==2.32.3
# mailsnake
# meilisearch
# openai
# openedx-forum
# optimizely-sdk
# pyjwkest
# pylti1p3

View File

@@ -119,6 +119,7 @@ openedx-calc # Library supporting mathematical calculatio
openedx-django-require
openedx-events # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)
openedx-forum # Open edX forum v2 application
openedx-learning # Open edX Learning core (experimental)
openedx-mongodbproxy
openedx-django-wiki

View File

@@ -87,6 +87,7 @@ beautifulsoup4==4.12.3
# via
# -r requirements/edx/base.txt
# -r requirements/edx/testing.in
# openedx-forum
# pynliner
billiard==4.2.1
# via
@@ -318,6 +319,7 @@ django==4.2.17
# openedx-django-wiki
# openedx-events
# openedx-filters
# openedx-forum
# openedx-learning
# ora2
# social-auth-app-django
@@ -483,6 +485,7 @@ djangorestframework==3.14.0
# edx-organizations
# edx-proctoring
# edx-submissions
# openedx-forum
# openedx-learning
# ora2
# super-csv
@@ -624,7 +627,9 @@ edx-rest-api-client==6.0.0
# edx-enterprise
# edx-proctoring
edx-search==4.1.1
# via -r requirements/edx/base.txt
# via
# -r requirements/edx/base.txt
# openedx-forum
edx-sga==0.25.0
# via -r requirements/edx/base.txt
edx-submissions==3.8.3
@@ -660,6 +665,7 @@ elasticsearch==7.9.1
# -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# edx-search
# openedx-forum
enmerkar==0.7.1
# via
# -r requirements/edx/base.txt
@@ -982,7 +988,9 @@ multidict==6.1.0
# aiohttp
# yarl
mysqlclient==2.2.6
# via -r requirements/edx/base.txt
# via
# -r requirements/edx/base.txt
# openedx-forum
newrelic==10.3.1
# via
# -r requirements/edx/base.txt
@@ -1018,7 +1026,9 @@ openai==0.28.1
# -r requirements/edx/base.txt
# edx-enterprise
openedx-atlas==0.6.2
# via -r requirements/edx/base.txt
# via
# -r requirements/edx/base.txt
# openedx-forum
openedx-calc==4.0.1
# via -r requirements/edx/base.txt
openedx-django-pyfs==3.7.0
@@ -1045,6 +1055,8 @@ openedx-filters==1.11.0
# -r requirements/edx/base.txt
# lti-consumer-xblock
# ora2
openedx-forum==0.1.5
# via -r requirements/edx/base.txt
openedx-learning==0.18.1
# via
# -c requirements/edx/../constraints.txt
@@ -1251,6 +1263,7 @@ pymongo==4.4.0
# edx-opaque-keys
# event-tracking
# mongoengine
# openedx-forum
# openedx-mongodbproxy
pynacl==1.5.0
# via
@@ -1407,6 +1420,7 @@ requests==2.32.3
# mailsnake
# meilisearch
# openai
# openedx-forum
# optimizely-sdk
# pact-python
# pyjwkest