Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions lms/djangoapps/discussion/rest_api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
23 changes: 0 additions & 23 deletions lms/djangoapps/discussion/rest_api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"<<Bulk Delete>> 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"<<Bulk Delete>> 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)
6 changes: 0 additions & 6 deletions lms/djangoapps/discussion/rest_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from rest_framework.routers import SimpleRouter

from lms.djangoapps.discussion.rest_api.views import (
BulkDeleteUserPosts,
CommentViewSet,
CourseActivityStatsView,
CourseDiscussionRolesAPIView,
Expand Down Expand Up @@ -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)),
]
75 changes: 0 additions & 75 deletions lms/djangoapps/discussion/rest_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"<<Bulk Delete>> {username} enrolled in {enrollments}")
log.info(f"<<Bulk Delete>> 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"<<Bulk Delete>> {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
)
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"<<Bulk Delete>> 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"<<Bulk Delete>> 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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"<<Bulk Delete>> 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"<<Bulk Delete>> 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):
Expand Down
Loading