From 019324ba0efc2580b0f29c73ea84e427913adefc Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed <68893403+taimoor-ahmed-1@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:17:39 +0500 Subject: [PATCH 1/3] fix: discussion xblock not compatible with forum v2 (#36315) fix all endpoints that were currently breaking with the discussion xblock. Co-authored-by: Taimoor Ahmed (cherry picked from commit 0d4281aa2eef677bcde2c149aa1b92ead13807b7) --- .../django_comment_client/base/tests.py | 2 +- .../django_comment_client/base/views.py | 24 +++++++++---------- lms/djangoapps/discussion/views.py | 2 +- .../comment_client/comment.py | 8 +++---- .../comment_client/models.py | 4 ++-- .../comment_client/thread.py | 8 ++++--- .../comment_client/user.py | 14 +++++------ 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests.py b/lms/djangoapps/discussion/django_comment_client/base/tests.py index df087fdc533e..d2a4f921f28b 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/tests.py +++ b/lms/djangoapps/discussion/django_comment_client/base/tests.py @@ -812,7 +812,7 @@ def test_update_comment_basic(self, mock_is_forum_v2_enabled, mock_request): headers=ANY, params=ANY, timeout=ANY, - data={"body": updated_body} + data={"body": updated_body, "course_id": str(self.course_id)} ) def test_flag_thread_open(self, mock_is_forum_v2_enabled, mock_request): diff --git a/lms/djangoapps/discussion/django_comment_client/base/views.py b/lms/djangoapps/discussion/django_comment_client/base/views.py index 3df362bdf6d2..55057060d310 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/views.py +++ b/lms/djangoapps/discussion/django_comment_client/base/views.py @@ -673,7 +673,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None): parent_id=parent_id, body=sanitize_body(post["body"]), ) - comment.save() + comment.save(params={"course_id": str(course_key)}) comment_created.send(sender=None, user=user, post=comment) @@ -736,7 +736,7 @@ def update_comment(request, course_id, comment_id): if 'body' not in request.POST or not request.POST['body'].strip(): return JsonError(_("Body can't be empty")) comment.body = sanitize_body(request.POST["body"]) - comment.save() + comment.save(params={"course_id": course_id}) comment_edited.send(sender=None, user=request.user, post=comment) @@ -762,7 +762,7 @@ def endorse_comment(request, course_id, comment_id): endorsed = request.POST.get('endorsed', 'false').lower() == 'true' comment.endorsed = endorsed comment.endorsement_user_id = user.id - comment.save() + comment.save(params={"course_id": course_id}) comment_endorsed.send(sender=None, user=user, post=comment) track_forum_response_mark_event(request, course, comment, endorsed) return JsonResponse(prepare_content(comment.to_dict(), course_key)) @@ -814,7 +814,7 @@ def delete_comment(request, course_id, comment_id): course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) comment = cc.Comment.find(comment_id) - comment.delete() + comment.delete(course_id=course_id) comment_deleted.send(sender=None, user=request.user, post=comment) track_comment_deleted_event(request, course, comment) return JsonResponse(prepare_content(comment.to_dict(), course_key)) @@ -828,12 +828,12 @@ def _vote_or_unvote(request, course_id, obj, value='up', undo_vote=False): course = get_course_with_access(request.user, 'load', course_key) user = cc.User.from_django_user(request.user) if undo_vote: - user.unvote(obj) + user.unvote(obj, course_id) # TODO(smarnach): Determine the value of the vote that is undone. Currently, you can # only cast upvotes in the user interface, so it is assumed that the vote value is 'up'. # (People could theoretically downvote by handcrafting AJAX requests.) else: - user.vote(obj, value) + user.vote(obj, value, course_id) thread_voted.send(sender=None, user=request.user, post=obj) track_voted_event(request, course, obj, value, undo_vote) return JsonResponse(prepare_content(obj.to_dict(), course_key)) @@ -899,7 +899,7 @@ def flag_abuse_for_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) course = get_course_by_id(course_key) thread = cc.Thread.find(thread_id) - thread.flagAbuse(user, thread) + thread.flagAbuse(user, thread, course_id) track_discussion_reported_event(request, course, thread) thread_flagged.send(sender='flag_abuse_for_thread', user=request.user, post=thread) return JsonResponse(prepare_content(thread.to_dict(), course_key)) @@ -921,7 +921,7 @@ def un_flag_abuse_for_thread(request, course_id, thread_id): has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course) ) - thread.unFlagAbuse(user, thread, remove_all) + thread.unFlagAbuse(user, thread, remove_all, course_id) track_discussion_unreported_event(request, course, thread) return JsonResponse(prepare_content(thread.to_dict(), course_key)) @@ -938,7 +938,7 @@ def flag_abuse_for_comment(request, course_id, comment_id): user = cc.User.from_django_user(request.user) course = get_course_by_id(course_key) comment = cc.Comment.find(comment_id) - comment.flagAbuse(user, comment) + comment.flagAbuse(user, comment, course_id) track_discussion_reported_event(request, course, comment) comment_flagged.send(sender='flag_abuse_for_comment', user=request.user, post=comment) return JsonResponse(prepare_content(comment.to_dict(), course_key)) @@ -960,7 +960,7 @@ def un_flag_abuse_for_comment(request, course_id, comment_id): has_access(request.user, 'staff', course) ) comment = cc.Comment.find(comment_id) - comment.unFlagAbuse(user, comment, remove_all) + comment.unFlagAbuse(user, comment, remove_all, course_id) track_discussion_unreported_event(request, course, comment) return JsonResponse(prepare_content(comment.to_dict(), course_key)) @@ -1005,7 +1005,7 @@ def follow_thread(request, course_id, thread_id): # lint-amnesty, pylint: disab course_key = CourseKey.from_string(course_id) course = get_course_by_id(course_key) thread = cc.Thread.find(thread_id) - user.follow(thread) + user.follow(thread, course_id=course_id) thread_followed.send(sender=None, user=request.user, post=thread) track_thread_followed_event(request, course, thread, True) return JsonResponse({}) @@ -1037,7 +1037,7 @@ def unfollow_thread(request, course_id, thread_id): # lint-amnesty, pylint: dis course = get_course_by_id(course_key) user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - user.unfollow(thread) + user.unfollow(thread, course_id=course_id) thread_unfollowed.send(sender=None, user=request.user, post=thread) track_thread_followed_event(request, course, thread, False) return JsonResponse({}) diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py index bfa511a575bd..54ac13c68ae2 100644 --- a/lms/djangoapps/discussion/views.py +++ b/lms/djangoapps/discussion/views.py @@ -146,7 +146,7 @@ def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS # If the user clicked a sort key, update their default sort key cc_user = cc.User.from_django_user(request.user) cc_user.default_sort_key = request.GET.get('sort_key') - cc_user.save() + cc_user.save(params={"course_id": course.id}) #there are 2 dimensions to consider when executing a search with respect to group id #is user a moderator diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py index 39b8680b6377..5f6348547efb 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py @@ -63,14 +63,14 @@ def url(cls, action, params=None): else: return super().url(action, params) - def flagAbuse(self, user, voteable): + def flagAbuse(self, user, voteable, course_id=None): if voteable.type == 'thread': url = _url_for_flag_abuse_thread(voteable.id) elif voteable.type == 'comment': url = _url_for_flag_abuse_comment(voteable.id) else: raise CommentClientRequestError("Can only flag/unflag threads or comments") - course_key = get_course_key(self.attributes.get("course_id")) + course_key = get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): if voteable.type == 'thread': response = forum_api.update_thread_flag( @@ -91,14 +91,14 @@ def flagAbuse(self, user, voteable): ) voteable._update_from_response(response) - def unFlagAbuse(self, user, voteable, removeAll): + def unFlagAbuse(self, user, voteable, removeAll, course_id=None): if voteable.type == 'thread': url = _url_for_unflag_abuse_thread(voteable.id) elif voteable.type == 'comment': url = _url_for_unflag_abuse_comment(voteable.id) else: raise CommentClientRequestError("Can flag/unflag for threads or comments") - course_key = get_course_key(self.attributes.get("course_id")) + course_key = get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): if voteable.type == "thread": response = forum_api.update_thread_flag( diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/models.py b/openedx/core/djangoapps/django_comment_common/comment_client/models.py index 9b6c9ca03f3d..fd8c10220d3a 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/models.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/models.py @@ -175,8 +175,8 @@ def save(self, params=None): self._update_from_response(response) self.after_save(self) - def delete(self): - course_key = get_course_key(self.attributes.get("course_id")) + def delete(self, course_id=None): + course_key = get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): response = None if self.type == "comment": diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py index b0d9a00f4063..0313d29daae9 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py @@ -80,6 +80,8 @@ def search(cls, query_params): search_params.pop('commentable_id', None) response = forum_api.search_threads(**search_params) else: + if user_id := params.get('user_id'): + params['user_id'] = str(user_id) response = forum_api.get_user_threads(**params) else: response = utils.perform_request( @@ -196,12 +198,12 @@ def _retrieve(self, *args, **kwargs): ) self._update_from_response(response) - def flagAbuse(self, user, voteable): + def flagAbuse(self, user, voteable, course_id=None): if voteable.type == 'thread': url = _url_for_flag_abuse_thread(voteable.id) else: raise utils.CommentClientRequestError("Can only flag/unflag threads or comments") - course_key = utils.get_course_key(self.attributes.get("course_id")) + course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): response = forum_api.update_thread_flag(voteable.id, "flag", user_id=user.id, course_id=str(course_key)) else: @@ -215,7 +217,7 @@ def flagAbuse(self, user, voteable): ) voteable._update_from_response(response) - def unFlagAbuse(self, user, voteable, removeAll): + def unFlagAbuse(self, user, voteable, removeAll, course_id=None): if voteable.type == 'thread': url = _url_for_unflag_abuse_thread(voteable.id) else: diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/user.py b/openedx/core/djangoapps/django_comment_common/comment_client/user.py index 2de4fbbfa95a..4fd8bac18203 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/user.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/user.py @@ -50,8 +50,8 @@ def read(self, source): metric_tags=self._metric_tags + [f'target.type:{source.type}'], ) - def follow(self, source): - course_key = utils.get_course_key(self.attributes.get("course_id")) + def follow(self, source, course_id=None): + course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): forum_api.create_subscription( user_id=self.id, @@ -68,8 +68,8 @@ def follow(self, source): metric_tags=self._metric_tags + [f'target.type:{source.type}'], ) - def unfollow(self, source): - course_key = utils.get_course_key(self.attributes.get("course_id")) + def unfollow(self, source, course_id=None): + course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): forum_api.delete_subscription( user_id=self.id, @@ -86,14 +86,14 @@ def unfollow(self, source): metric_tags=self._metric_tags + [f'target.type:{source.type}'], ) - def vote(self, voteable, value): + def vote(self, voteable, value, course_id=None): if voteable.type == 'thread': url = _url_for_vote_thread(voteable.id) elif voteable.type == 'comment': url = _url_for_vote_comment(voteable.id) else: raise utils.CommentClientRequestError("Can only vote / unvote for threads or comments") - course_key = utils.get_course_key(self.attributes.get("course_id")) + course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) if is_forum_v2_enabled(course_key): if voteable.type == 'thread': response = forum_api.update_thread_votes( @@ -120,7 +120,7 @@ def vote(self, voteable, value): ) voteable._update_from_response(response) - def unvote(self, voteable): + def unvote(self, voteable, course_id=None): if voteable.type == 'thread': url = _url_for_vote_thread(voteable.id) elif voteable.type == 'comment': From 1f60cdb7da336f3db6f97d3447a204db3a430f45 Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed <68893403+taimoor-ahmed-1@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:04:14 +0200 Subject: [PATCH 2/3] fix: legacy forum issues (#36470) Co-authored-by: Taimoor Ahmed (cherry picked from commit 0595e5a57da1732feb9dfc416e4eef5d683162ab) --- .../discussion/django_comment_client/base/views.py | 2 +- lms/djangoapps/discussion/views.py | 8 ++++---- .../django_comment_common/comment_client/models.py | 6 +++--- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lms/djangoapps/discussion/django_comment_client/base/views.py b/lms/djangoapps/discussion/django_comment_client/base/views.py index 55057060d310..511ca255d4f9 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/views.py +++ b/lms/djangoapps/discussion/django_comment_client/base/views.py @@ -584,7 +584,7 @@ def create_thread(request, course_id, commentable_id): if follow: cc_user = cc.User.from_django_user(user) - cc_user.follow(thread) + cc_user.follow(thread, course_id) thread_followed.send(sender=None, user=user, post=thread) data = thread.to_dict() diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py index 54ac13c68ae2..3a3c8c133471 100644 --- a/lms/djangoapps/discussion/views.py +++ b/lms/djangoapps/discussion/views.py @@ -146,7 +146,7 @@ def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS # If the user clicked a sort key, update their default sort key cc_user = cc.User.from_django_user(request.user) cc_user.default_sort_key = request.GET.get('sort_key') - cc_user.save(params={"course_id": course.id}) + cc_user.save(params={"course_id": str(course.id)}) #there are 2 dimensions to consider when executing a search with respect to group id #is user a moderator @@ -218,7 +218,7 @@ def inline_discussion(request, course_key, discussion_id): with function_trace('get_course_and_user_info'): course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) cc_user = cc.User.from_django_user(request.user) - user_info = cc_user.to_dict() + user_info = cc_user.to_dict(course_key=str(course_key)) try: with function_trace('get_threads'): @@ -356,7 +356,7 @@ def single_thread(request, course_key, discussion_id, thread_id): if request.headers.get('x-requested-with') == 'XMLHttpRequest': cc_user = cc.User.from_django_user(request.user) - user_info = cc_user.to_dict() + user_info = cc_user.to_dict(course_key=str(course_key)) is_staff = has_permission(request.user, 'openclose_thread', course.id) try: @@ -471,7 +471,7 @@ def _create_base_discussion_view_context(request, course_key): """ user = request.user cc_user = cc.User.from_django_user(user) - user_info = cc_user.to_dict() + user_info = cc_user.to_dict(course_key=str(course_key)) course = get_course_with_access(user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, user) return { diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/models.py b/openedx/core/djangoapps/django_comment_common/comment_client/models.py index fd8c10220d3a..1812d24dec0a 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/models.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/models.py @@ -61,8 +61,8 @@ def items(self, *args, **kwargs): def get(self, *args, **kwargs): return self.attributes.get(*args, **kwargs) - def to_dict(self): - self.retrieve() + def to_dict(self, course_key=None): + self.retrieve(course_key=course_key) return self.attributes def retrieve(self, *args, **kwargs): @@ -72,7 +72,7 @@ def retrieve(self, *args, **kwargs): return self def _retrieve(self, *args, **kwargs): - course_id = self.attributes.get("course_id") or kwargs.get("course_id") + course_id = self.attributes.get("course_id") or kwargs.get("course_key") if course_id: course_key = get_course_key(course_id) use_forumv2 = is_forum_v2_enabled(course_key) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index f52e292a1b74..2eaf18e3b84e 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -838,7 +838,7 @@ openedx-filters==1.11.0 # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-forum==0.1.5 +openedx-forum==0.2.0 # via -r requirements/edx/kernel.in openedx-learning==0.18.1 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index acc7a6a4eed2..55a56512e41a 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1393,7 +1393,7 @@ openedx-filters==1.11.0 # -r requirements/edx/testing.txt # lti-consumer-xblock # ora2 -openedx-forum==0.1.5 +openedx-forum==0.2.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index ca313b416863..9d90c5faf4d8 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1007,7 +1007,7 @@ openedx-filters==1.11.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-forum==0.1.5 +openedx-forum==0.2.0 # via -r requirements/edx/base.txt openedx-learning==0.18.1 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index d86def193df3..e7aa45141e39 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1052,7 +1052,7 @@ openedx-filters==1.11.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-forum==0.1.5 +openedx-forum==0.2.0 # via -r requirements/edx/base.txt openedx-learning==0.18.1 # via From 29c5588925b825dc998476a5b1a86e966b66e463 Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Tue, 8 Apr 2025 19:00:56 +0200 Subject: [PATCH 3/3] build: Switch off deprecated C-Hive NPM cache (#36502) JS tests are failing because we are using a discontinued GHA caching service: https://github.blog/changelog/2025-03-20-notification-of-upcoming-breaking-changes-in-github-actions/#decommissioned-cache-service-brownouts This service is used by the unsupported C-Hive caching action which we are relying on: https://github.com/c-hive/gha-npm-cache We are switching to the supported caching mechanims which is provided by setup-node: https://github.com/actions/setup-node?tab=readme-ov-file#caching-global-packages-data (cherry picked from commit 0120179531d1cedd41c6dec237616cf6567d8c18) --- .github/workflows/js-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/js-tests.yml b/.github/workflows/js-tests.yml index c9d2d7ab1191..66b864a6895e 100644 --- a/.github/workflows/js-tests.yml +++ b/.github/workflows/js-tests.yml @@ -26,6 +26,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + cache: 'npm' - name: Setup npm run: npm i -g npm@10.5.x @@ -63,7 +64,6 @@ jobs: run: | make base-requirements - - uses: c-hive/gha-npm-cache@v1 - name: Run JS Tests env: TEST_SUITE: js-unit