Skip to content

Commit 4a75633

Browse files
Muhammad Faraz  MaqsoodFaraz32123[Ali Salman]
authored andcommitted
feat: Integrate Forum V2 into edx-platform
This commit introduces the new Forum V2 application, allowing users to choose between the legacy Forum V1 and the new Forum V2 at the course level. Key Changes: - Added waffle flag `forum_v2.enable_forum_v2` to enable Forum V2 for selected courses, allowing coexistence with Forum V1. - Default data storage for Forum V2 is set to MongoDB, with an option to switch to MySQL using the waffle flag `forum_v2.enable_mysql_backend`. - Introduced management command `forum_migrate_course_from_mongodb_to_mysql` for per-course data migration from MongoDB to MySQL. Note: This PR does not include all unit tests for the Forum V2 native API due to ongoing migration efforts. Further updates will follow to ensure full test coverage before final release. Co-authored-by: [Muhammad Faraz Maqsood] <[email protected]> Co-authored-by: [Ali Salman] <[email protected]>
1 parent cdf7a99 commit 4a75633

File tree

26 files changed

+1676
-440
lines changed

26 files changed

+1676
-440
lines changed

lms/djangoapps/discussion/django_comment_client/base/tests.py

Lines changed: 288 additions & 110 deletions
Large diffs are not rendered by default.

lms/djangoapps/discussion/django_comment_client/base/views.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,6 @@ def create_thread(request, course_id, commentable_id):
562562
params['context'] = ThreadContext.STANDALONE
563563
else:
564564
params['context'] = ThreadContext.COURSE
565-
566565
thread = cc.Thread(**params)
567566

568567
# Divide the thread if required

lms/djangoapps/discussion/django_comment_client/tests/group_id.py

Lines changed: 105 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -60,51 +60,76 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
6060
Provides test cases to verify that views pass the correct `group_id` to
6161
the comments service when requesting content in cohorted discussions.
6262
"""
63-
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
63+
def call_view(self, mock_is_forum_v2_enabled, mock_request, commentable_id, user, group_id, pass_group_id=True):
6464
"""
6565
Call the view for the implementing test class, constructing a request
6666
from the parameters.
6767
"""
6868
pass # lint-amnesty, pylint: disable=unnecessary-pass
6969

70-
def test_cohorted_topic_student_without_group_id(self, mock_request):
71-
self.call_view(mock_request, "cohorted_topic", self.student, '', pass_group_id=False)
70+
def test_cohorted_topic_student_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
71+
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.student, '', pass_group_id=False)
7272
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
7373

74-
def test_cohorted_topic_student_none_group_id(self, mock_request):
75-
self.call_view(mock_request, "cohorted_topic", self.student, "")
74+
def test_cohorted_topic_student_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
75+
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.student, "")
7676
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
7777

78-
def test_cohorted_topic_student_with_own_group_id(self, mock_request):
79-
self.call_view(mock_request, "cohorted_topic", self.student, self.student_cohort.id)
78+
def test_cohorted_topic_student_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
79+
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.student, self.student_cohort.id)
8080
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
8181

82-
def test_cohorted_topic_student_with_other_group_id(self, mock_request):
83-
self.call_view(mock_request, "cohorted_topic", self.student, self.moderator_cohort.id)
82+
def test_cohorted_topic_student_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
83+
self.call_view(
84+
mock_is_forum_v2_enabled,
85+
mock_request,
86+
"cohorted_topic",
87+
self.student,
88+
self.moderator_cohort.id
89+
)
8490
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
8591

86-
def test_cohorted_topic_moderator_without_group_id(self, mock_request):
87-
self.call_view(mock_request, "cohorted_topic", self.moderator, '', pass_group_id=False)
92+
def test_cohorted_topic_moderator_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
93+
self.call_view(
94+
mock_is_forum_v2_enabled,
95+
mock_request,
96+
"cohorted_topic",
97+
self.moderator,
98+
'',
99+
pass_group_id=False
100+
)
88101
self._assert_comments_service_called_without_group_id(mock_request)
89102

90-
def test_cohorted_topic_moderator_none_group_id(self, mock_request):
91-
self.call_view(mock_request, "cohorted_topic", self.moderator, "")
103+
def test_cohorted_topic_moderator_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
104+
self.call_view(mock_is_forum_v2_enabled, mock_request, "cohorted_topic", self.moderator, "")
92105
self._assert_comments_service_called_without_group_id(mock_request)
93106

94-
def test_cohorted_topic_moderator_with_own_group_id(self, mock_request):
95-
self.call_view(mock_request, "cohorted_topic", self.moderator, self.moderator_cohort.id)
107+
def test_cohorted_topic_moderator_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
108+
self.call_view(
109+
mock_is_forum_v2_enabled,
110+
mock_request,
111+
"cohorted_topic",
112+
self.moderator,
113+
self.moderator_cohort.id
114+
)
96115
self._assert_comments_service_called_with_group_id(mock_request, self.moderator_cohort.id)
97116

98-
def test_cohorted_topic_moderator_with_other_group_id(self, mock_request):
99-
self.call_view(mock_request, "cohorted_topic", self.moderator, self.student_cohort.id)
117+
def test_cohorted_topic_moderator_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
118+
self.call_view(
119+
mock_is_forum_v2_enabled,
120+
mock_request,
121+
"cohorted_topic",
122+
self.moderator,
123+
self.student_cohort.id
124+
)
100125
self._assert_comments_service_called_with_group_id(mock_request, self.student_cohort.id)
101126

102-
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
127+
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_is_forum_v2_enabled, mock_request):
103128
invalid_id = self.student_cohort.id + self.moderator_cohort.id
104-
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id) # lint-amnesty, pylint: disable=assignment-from-no-return
129+
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
105130
assert response.status_code == 500
106131

107-
def test_cohorted_topic_enrollment_track_invalid_group_id(self, mock_request):
132+
def test_cohorted_topic_enrollment_track_invalid_group_id(self, mock_is_forum_v2_enabled, mock_request):
108133
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
109134
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
110135
discussion_settings = CourseDiscussionSettings.get(self.course.id)
@@ -115,7 +140,7 @@ def test_cohorted_topic_enrollment_track_invalid_group_id(self, mock_request):
115140
})
116141

117142
invalid_id = -1000
118-
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id) # lint-amnesty, pylint: disable=assignment-from-no-return
143+
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
119144
assert response.status_code == 500
120145

121146

@@ -124,57 +149,95 @@ class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
124149
Provides test cases to verify that views pass the correct `group_id` to
125150
the comments service when requesting content in non-cohorted discussions.
126151
"""
127-
def call_view(self, mock_request, commentable_id, user, group_id, pass_group_id=True):
152+
def call_view(self, mock_is_forum_v2_enabled, mock_request, commentable_id, user, group_id, pass_group_id=True):
128153
"""
129154
Call the view for the implementing test class, constructing a request
130155
from the parameters.
131156
"""
132157
pass # lint-amnesty, pylint: disable=unnecessary-pass
133158

134-
def test_non_cohorted_topic_student_without_group_id(self, mock_request):
135-
self.call_view(mock_request, "non_cohorted_topic", self.student, '', pass_group_id=False)
159+
def test_non_cohorted_topic_student_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
160+
self.call_view(
161+
mock_is_forum_v2_enabled,
162+
mock_request,
163+
"non_cohorted_topic",
164+
self.student,
165+
'',
166+
pass_group_id=False
167+
)
136168
self._assert_comments_service_called_without_group_id(mock_request)
137169

138-
def test_non_cohorted_topic_student_none_group_id(self, mock_request):
139-
self.call_view(mock_request, "non_cohorted_topic", self.student, '')
170+
def test_non_cohorted_topic_student_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
171+
self.call_view(mock_is_forum_v2_enabled, mock_request, "non_cohorted_topic", self.student, '')
140172
self._assert_comments_service_called_without_group_id(mock_request)
141173

142-
def test_non_cohorted_topic_student_with_own_group_id(self, mock_request):
143-
self.call_view(mock_request, "non_cohorted_topic", self.student, self.student_cohort.id)
174+
def test_non_cohorted_topic_student_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
175+
self.call_view(
176+
mock_is_forum_v2_enabled,
177+
mock_request,
178+
"non_cohorted_topic",
179+
self.student,
180+
self.student_cohort.id
181+
)
144182
self._assert_comments_service_called_without_group_id(mock_request)
145183

146-
def test_non_cohorted_topic_student_with_other_group_id(self, mock_request):
147-
self.call_view(mock_request, "non_cohorted_topic", self.student, self.moderator_cohort.id)
184+
def test_non_cohorted_topic_student_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
185+
self.call_view(
186+
mock_is_forum_v2_enabled,
187+
mock_request,
188+
"non_cohorted_topic",
189+
self.student,
190+
self.moderator_cohort.id
191+
)
148192
self._assert_comments_service_called_without_group_id(mock_request)
149193

150-
def test_non_cohorted_topic_moderator_without_group_id(self, mock_request):
151-
self.call_view(mock_request, "non_cohorted_topic", self.moderator, '', pass_group_id=False)
194+
def test_non_cohorted_topic_moderator_without_group_id(self, mock_is_forum_v2_enabled, mock_request):
195+
self.call_view(
196+
mock_is_forum_v2_enabled,
197+
mock_request,
198+
"non_cohorted_topic",
199+
self.moderator,
200+
"",
201+
pass_group_id=False,
202+
)
152203
self._assert_comments_service_called_without_group_id(mock_request)
153204

154-
def test_non_cohorted_topic_moderator_none_group_id(self, mock_request):
155-
self.call_view(mock_request, "non_cohorted_topic", self.moderator, '')
205+
def test_non_cohorted_topic_moderator_none_group_id(self, mock_is_forum_v2_enabled, mock_request):
206+
self.call_view(mock_is_forum_v2_enabled, mock_request, "non_cohorted_topic", self.moderator, '')
156207
self._assert_comments_service_called_without_group_id(mock_request)
157208

158-
def test_non_cohorted_topic_moderator_with_own_group_id(self, mock_request):
159-
self.call_view(mock_request, "non_cohorted_topic", self.moderator, self.moderator_cohort.id)
209+
def test_non_cohorted_topic_moderator_with_own_group_id(self, mock_is_forum_v2_enabled, mock_request):
210+
self.call_view(
211+
mock_is_forum_v2_enabled,
212+
mock_request,
213+
"non_cohorted_topic",
214+
self.moderator,
215+
self.moderator_cohort.id,
216+
)
160217
self._assert_comments_service_called_without_group_id(mock_request)
161218

162-
def test_non_cohorted_topic_moderator_with_other_group_id(self, mock_request):
163-
self.call_view(mock_request, "non_cohorted_topic", self.moderator, self.student_cohort.id)
219+
def test_non_cohorted_topic_moderator_with_other_group_id(self, mock_is_forum_v2_enabled, mock_request):
220+
self.call_view(
221+
mock_is_forum_v2_enabled,
222+
mock_request,
223+
"non_cohorted_topic",
224+
self.moderator,
225+
self.student_cohort.id,
226+
)
164227
self._assert_comments_service_called_without_group_id(mock_request)
165228

166-
def test_non_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
229+
def test_non_cohorted_topic_moderator_with_invalid_group_id(self, mock_is_forum_v2_enabled, mock_request):
167230
invalid_id = self.student_cohort.id + self.moderator_cohort.id
168-
self.call_view(mock_request, "non_cohorted_topic", self.moderator, invalid_id)
231+
self.call_view(mock_is_forum_v2_enabled, mock_request, "non_cohorted_topic", self.moderator, invalid_id)
169232
self._assert_comments_service_called_without_group_id(mock_request)
170233

171-
def test_team_discussion_id_not_cohorted(self, mock_request):
234+
def test_team_discussion_id_not_cohorted(self, mock_is_forum_v2_enabled, mock_request):
172235
team = CourseTeamFactory(
173236
course_id=self.course.id,
174237
topic_id='topic-id'
175238
)
176239

177240
team.add_user(self.student)
178-
self.call_view(mock_request, team.discussion_topic_id, self.student, '')
241+
self.call_view(mock_is_forum_v2_enabled, mock_request, team.discussion_topic_id, self.student, '')
179242

180243
self._assert_comments_service_called_without_group_id(mock_request)

lms/djangoapps/discussion/rest_api/api.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def _get_course(course_key: CourseKey, user: User, check_tab: bool = True) -> Co
199199
return course
200200

201201

202-
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
202+
def _get_thread_and_context(request, thread_id, retrieve_kwargs=None, course_id=None):
203203
"""
204204
Retrieve the given thread and build a serializer context for it, returning
205205
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):
213213
retrieve_kwargs["with_responses"] = False
214214
if "mark_as_read" not in retrieve_kwargs:
215215
retrieve_kwargs["mark_as_read"] = False
216-
cc_thread = Thread(id=thread_id).retrieve(**retrieve_kwargs)
216+
cc_thread = Thread(id=thread_id).retrieve(course_id=course_id, **retrieve_kwargs)
217217
course_key = CourseKey.from_string(cc_thread["course_id"])
218218
course = _get_course(course_key, request.user)
219219
context = get_context(course, request, cc_thread)
@@ -1645,7 +1645,8 @@ def get_thread(request, thread_id, requested_fields=None, course_id=None):
16451645
retrieve_kwargs={
16461646
"with_responses": True,
16471647
"user_id": str(request.user.id),
1648-
}
1648+
},
1649+
course_id=course_id,
16491650
)
16501651
if course_id and course_id != cc_thread.course_id:
16511652
raise ThreadNotFoundError("Thread not found.")

lms/djangoapps/discussion/rest_api/discussions_notifications.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def send_response_on_followed_post_notification(self):
203203

204204
while has_more_subscribers:
205205

206-
subscribers = Subscription.fetch(self.thread.id, query_params={'page': page})
206+
subscribers = Subscription.fetch(self.thread.id, self.course.id, query_params={'page': page})
207207
if page <= subscribers.num_pages:
208208
for subscriber in subscribers.collection:
209209
# Check if the subscriber is not the thread creator or response creator

lms/djangoapps/discussion/rest_api/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_context(course, request, thread=None):
6868
moderator_user_ids = get_moderator_users_list(course.id)
6969
ta_user_ids = get_course_ta_users_list(course.id)
7070
requester = request.user
71-
cc_requester = CommentClientUser.from_django_user(requester).retrieve()
71+
cc_requester = CommentClientUser.from_django_user(requester).retrieve(course_id=course.id)
7272
cc_requester["course_id"] = course.id
7373
course_discussion_settings = CourseDiscussionSettings.get(course.id)
7474
is_global_staff = GlobalStaff().has_user(requester)

0 commit comments

Comments
 (0)