diff --git a/lms/djangoapps/discussion/rest_api/permissions.py b/lms/djangoapps/discussion/rest_api/permissions.py index 77117aa60418..26421340d772 100644 --- a/lms/djangoapps/discussion/rest_api/permissions.py +++ b/lms/djangoapps/discussion/rest_api/permissions.py @@ -210,17 +210,3 @@ def can_take_action_on_spam(user, course_id): if CourseAccessRole.objects.filter(user=user, course_id__in=course_ids, role__in=["instructor", "staff"]).exists(): return True return False - - -class IsAllowedToBulkDelete(permissions.BasePermission): - """ - Permission that checks if the user is staff or an admin. - """ - - def has_permission(self, request, view): - """Returns true if the user can bulk delete posts""" - if not request.user.is_authenticated: - return False - - course_id = view.kwargs.get("course_id") - return can_take_action_on_spam(request.user, course_id) diff --git a/lms/djangoapps/discussion/rest_api/tasks.py b/lms/djangoapps/discussion/rest_api/tasks.py index abbf4556be80..767daa46483e 100644 --- a/lms/djangoapps/discussion/rest_api/tasks.py +++ b/lms/djangoapps/discussion/rest_api/tasks.py @@ -6,11 +6,9 @@ from celery import shared_task from django.contrib.auth import get_user_model from edx_django_utils.monitoring import set_code_owner_attribute -from eventtracking import tracker from opaque_keys.edx.locator import CourseKey from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole -from common.djangoapps.track import segment from lms.djangoapps.courseware.courses import get_course_with_access from lms.djangoapps.discussion.django_comment_client.utils import get_user_role_names from lms.djangoapps.discussion.rest_api.discussions_notifications import DiscussionNotificationSender @@ -89,24 +87,3 @@ def send_response_endorsed_notifications(thread_id, response_id, course_key_str, if int(response.user_id) != endorser.id: notification_sender.creator = User.objects.get(id=response.user_id) notification_sender.send_response_endorsed_notification() - - -@shared_task -@set_code_owner_attribute -def delete_course_post_for_user(user_id, username, course_ids, event_data=None): - """ - Deletes all posts for user in a course. - """ - event_data = event_data or {} - log.info(f"<> Deleting all posts for {username} in course {course_ids}") - threads_deleted = Thread.delete_user_threads(user_id, course_ids) - comments_deleted = Comment.delete_user_comments(user_id, course_ids) - log.info(f"<> Deleted {threads_deleted} posts and {comments_deleted} comments for {username} " - f"in course {course_ids}") - event_data.update({ - "number_of_posts_deleted": threads_deleted, - "number_of_comments_deleted": comments_deleted, - }) - event_name = 'edx.discussion.bulk_delete_user_posts' - tracker.emit(event_name, event_data) - segment.track('None', event_name, event_data) diff --git a/lms/djangoapps/discussion/rest_api/urls.py b/lms/djangoapps/discussion/rest_api/urls.py index 082fdabc608d..2ec852b5959d 100644 --- a/lms/djangoapps/discussion/rest_api/urls.py +++ b/lms/djangoapps/discussion/rest_api/urls.py @@ -8,7 +8,6 @@ from rest_framework.routers import SimpleRouter from lms.djangoapps.discussion.rest_api.views import ( - BulkDeleteUserPosts, CommentViewSet, CourseActivityStatsView, CourseDiscussionRolesAPIView, @@ -88,10 +87,5 @@ CourseTopicsViewV3.as_view(), name="course_topics_v3" ), - re_path( - fr"^v1/bulk_delete_user_posts/{settings.COURSE_ID_PATTERN}", - BulkDeleteUserPosts.as_view(), - name="bulk_delete_user_posts" - ), path('v1/', include(ROUTER.urls)), ] diff --git a/lms/djangoapps/discussion/rest_api/views.py b/lms/djangoapps/discussion/rest_api/views.py index 992fe91f2d45..e87f34958e61 100644 --- a/lms/djangoapps/discussion/rest_api/views.py +++ b/lms/djangoapps/discussion/rest_api/views.py @@ -20,23 +20,18 @@ from rest_framework.views import APIView from rest_framework.viewsets import ViewSet -from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.util.file import store_uploaded_file from lms.djangoapps.course_api.blocks.api import get_blocks from lms.djangoapps.course_goals.models import UserActivity from lms.djangoapps.discussion.django_comment_client import settings as cc_settings from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service from lms.djangoapps.discussion.rate_limit import is_content_creation_rate_limited -from lms.djangoapps.discussion.rest_api.permissions import IsAllowedToBulkDelete -from lms.djangoapps.discussion.rest_api.tasks import delete_course_post_for_user from lms.djangoapps.discussion.toggles import ONLY_VERIFIED_USERS_CAN_POST from lms.djangoapps.instructor.access import update_forum_role from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider from openedx.core.djangoapps.discussions.serializers import DiscussionSettingsSerializer from openedx.core.djangoapps.django_comment_common import comment_client -from openedx.core.djangoapps.django_comment_common.comment_client.comment import Comment -from openedx.core.djangoapps.django_comment_common.comment_client.thread import Thread from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings, Role from openedx.core.djangoapps.user_api.accounts.permissions import CanReplaceUsername, CanRetireUser from openedx.core.djangoapps.user_api.models import UserRetirementStatus @@ -1543,73 +1538,3 @@ def post(self, request, course_id, rolename): context = {'course_discussion_settings': CourseDiscussionSettings.get(course_id)} serializer = DiscussionRolesListSerializer(data, context=context) return Response(serializer.data) - - -class BulkDeleteUserPosts(DeveloperErrorViewMixin, APIView): - """ - **Use Cases** - A privileged user that can delete all posts and comments made by a user. - It returns expected number of comments and threads that will be deleted - - **Example Requests**: - POST /api/discussion/v1/bulk_delete_user_posts/{course_id} - Query Parameters: - username: The username of the user whose posts are to be deleted - course_id: Course id for which posts are to be removed - execute: If True, runs deletion task - course_or_org: If 'course', deletes posts in the course, if 'org', deletes posts in all courses of the org - - **Example Response**: - Empty string - """ - - authentication_classes = ( - JwtAuthentication, BearerAuthentication, SessionAuthentication, - ) - permission_classes = (permissions.IsAuthenticated, IsAllowedToBulkDelete) - - def post(self, request, course_id): - """ - Implements the delete user posts endpoint. - TODO: Add support for MySQLBackend as well - """ - username = request.GET.get("username", None) - execute_task = request.GET.get("execute", "false").lower() == "true" - if (not username) or (not course_id): - raise BadRequest("username and course_id are required.") - course_or_org = request.GET.get("course_or_org", "course") - if course_or_org not in ["course", "org"]: - raise BadRequest("course_or_org must be either 'course' or 'org'.") - - user = get_object_or_404(User, username=username) - course_ids = [course_id] - if course_or_org == "org": - org_id = CourseKey.from_string(course_id).org - enrollments = CourseEnrollment.objects.filter(user=request.user).values_list('course_id', flat=True) - course_ids.extend([ - str(c_id) - for c_id in enrollments - if c_id.org == org_id - ]) - course_ids = list(set(course_ids)) - log.info(f"<> {username} enrolled in {enrollments}") - log.info(f"<> Posts for {username} in {course_ids} - for {course_or_org} {course_id}") - - comment_count = Comment.get_user_comment_count(user.id, course_ids) - thread_count = Thread.get_user_threads_count(user.id, course_ids) - log.info(f"<> {username} in {course_ids} - Count thread {thread_count}, comment {comment_count}") - - if execute_task: - event_data = { - "triggered_by": request.user.username, - "username": username, - "course_or_org": course_or_org, - "course_key": course_id, - } - delete_course_post_for_user.apply_async( - args=(user.id, username, course_ids, event_data), - ) - return Response( - {"comment_count": comment_count, "thread_count": thread_count}, - status=status.HTTP_202_ACCEPTED - ) 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 30b27dba27c4..bd74b895113b 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py @@ -1,10 +1,8 @@ # pylint: disable=missing-docstring,protected-access import logging -import time from bs4 import BeautifulSoup from forum import api as forum_api -from forum.backends.mongodb.comments import Comment as ForumComment from openedx.core.djangoapps.django_comment_common.comment_client import models, settings @@ -129,43 +127,6 @@ def retrieve_all(cls, params=None): per_page=params.get('per_page', 10), ) - @classmethod - def get_user_comment_count(cls, user_id, course_ids): - """ - Returns comments and responses count of user in the given course_ids. - TODO: Add support for MySQL backend as well - """ - query_params = { - "course_id": {"$in": course_ids}, - "author_id": str(user_id), - "_type": "Comment" - } - return ForumComment()._collection.count_documents(query_params) # pylint: disable=protected-access - - @classmethod - def delete_user_comments(cls, user_id, course_ids): - """ - Deletes comments and responses of user in the given course_ids. - TODO: Add support for MySQL backend as well - """ - start_time = time.time() - query_params = { - "course_id": {"$in": course_ids}, - "author_id": str(user_id), - } - comments_deleted = 0 - comments = ForumComment().get_list(**query_params) - log.info(f"<> Fetched comments for user {user_id} in {time.time() - start_time} seconds") - for comment in comments: - start_time = time.time() - comment_id = comment.get("_id") - course_id = comment.get("course_id") - if comment_id: - forum_api.delete_comment(comment_id, course_id=course_id) - comments_deleted += 1 - log.info(f"<> Deleted comment {comment_id} in {time.time() - start_time} seconds." - f" Comment Found: {comment_id is not None}") - return comments_deleted def _url_for_thread_comments(thread_id): 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 31f4319f2301..516fc59e03ec 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/thread.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/thread.py @@ -2,11 +2,9 @@ import logging -import time from eventtracking import tracker from forum import api as forum_api -from forum.backends.mongodb.threads import CommentThread as ForumThread from . import models, settings, utils @@ -216,43 +214,6 @@ def un_pin(self, user, thread_id, course_id=None): ) self._update_from_response(response) - @classmethod - def get_user_threads_count(cls, user_id, course_ids): - """ - Returns threads count of user in the given course_ids. - TODO: Add support for MySQL backend as well - """ - query_params = { - "course_id": {"$in": course_ids}, - "author_id": str(user_id), - "_type": "CommentThread" - } - return ForumThread()._collection.count_documents(query_params) # pylint: disable=protected-access - - @classmethod - def delete_user_threads(cls, user_id, course_ids): - """ - Deletes threads of user in the given course_ids. - TODO: Add support for MySQL backend as well - """ - start_time = time.time() - query_params = { - "course_id": {"$in": course_ids}, - "author_id": str(user_id), - } - threads_deleted = 0 - threads = ForumThread().get_list(**query_params) - log.info(f"<> Fetched threads for user {user_id} in {time.time() - start_time} seconds") - for thread in threads: - start_time = time.time() - thread_id = thread.get("_id") - course_id = thread.get("course_id") - if thread_id: - forum_api.delete_thread(thread_id, course_id=course_id) - threads_deleted += 1 - log.info(f"<> Deleted thread {thread_id} in {time.time() - start_time} seconds." - f" Thread Found: {thread_id is not None}") - return threads_deleted def _clean_forum_params(params):