Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Implement an API with a task to delete site data. #613

Open
wants to merge 3 commits into
base: develop-koa
Choose a base branch
from
Open
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
11 changes: 9 additions & 2 deletions cms/djangoapps/contentstore/management/commands/delete_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def add_arguments(self, parser):
'Be careful! These assets may be associated with another course',
)

parser.add_argument(
'--force',
action='store_true',
default=False,
help='Skip confirmation prompts and delete immediately',
)

def handle(self, *args, **options):
try:
# a course key may have unicode chars in it
Expand All @@ -75,8 +82,8 @@ def handle(self, *args, **options):

print(u'Preparing to delete course %s from module store....' % options['course_key'])

if query_yes_no(u'Are you sure you want to delete course {}?'.format(course_key), default='no'):
if query_yes_no(u'Are you sure? This action cannot be undone!', default='no'):
if options['force'] or query_yes_no(u'Are you sure you want to delete course {}?'.format(course_key), default='no'):
if options['force'] or query_yes_no(u'Are you sure? This action cannot be undone!', default='no'):
delete_course(course_key, ModuleStoreEnum.UserID.mgmt_command, options['keep_instructors'])

if options['remove_assets']:
Expand Down
17 changes: 17 additions & 0 deletions openedx/features/edly/api/v1/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.db.models import Count
from openedx.features.edly.models import EdlyMultiSiteAccess

def get_users_for_site(sub_org):
"""
Get users for a site, excluding those linked with multiple sites.
"""
users = EdlyMultiSiteAccess.objects.filter(
sub_org=sub_org
).values_list('user', flat=True)

users_obj = EdlyMultiSiteAccess.objects.filter(
user__in=users
).values('user', 'user__username', 'user__email').annotate(
site_count=Count('id')
)
return users_obj
11 changes: 10 additions & 1 deletion openedx/features/edly/api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
from rest_framework import routers
from django.conf.urls import url

from openedx.features.edly.api.v1.views.course_enrollments import EdlyCourseEnrollmentViewSet
from openedx.features.edly.api.v1.views.enrollment_count import EdlyProgramEnrollmentCountViewSet
from openedx.features.edly.api.v1.views.user_mutisites import MultisitesViewset
from openedx.features.edly.api.v1.views.user_mutisites import (
MultisitesViewset,
EdlySiteUsersViewSet,
EdlySiteDeletionViewSet
)
from openedx.features.edly.api.v1.views.user_paid_for_course import UserPaidForCourseViewSet
from openedx.features.edly.api.v1.views.user_sites import UserSitesViewSet

router = routers.SimpleRouter()
router.register(r'user_sites', UserSitesViewSet, base_name='user_sites')
router.register(r'user_link_sites', MultisitesViewset, base_name='mutisite_access')
router.register(r'site_users', EdlySiteUsersViewSet, base_name='site_users')

router.register(
r'courses/course_enrollment',
Expand All @@ -29,3 +35,6 @@
)

urlpatterns = router.urls
urlpatterns += [
url(r'^delete_site/', EdlySiteDeletionViewSet.as_view({'post': 'post'}), name='delete_site')
]
58 changes: 57 additions & 1 deletion openedx/features/edly/api/v1/views/user_mutisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
from rest_framework import permissions, viewsets
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication
from django.contrib.sites.shortcuts import get_current_site
from django.core.management import call_command
from django.db.models import Case, IntegerField, When, Value
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication

from openedx.features.edly.api.serializers import MutiSiteAccessSerializer
from openedx.core.lib.api.authentication import BearerAuthentication
from openedx.features.edly.models import EdlyMultiSiteAccess
from openedx.features.edly.models import EdlySubOrganization, EdlyMultiSiteAccess
from openedx.features.edly.utils import get_edly_sub_org_from_request
from openedx.features.edly.api.v1.helper import get_users_for_site


class MultisitesViewset(viewsets.ViewSet):
Expand Down Expand Up @@ -58,3 +62,55 @@ def list(self, request, *args, **kwargs):

serializer = MutiSiteAccessSerializer(queryset, many=True)
return Response(serializer.data)


class EdlySiteUsersViewSet(viewsets.ViewSet):
"""
**Use Case**

Get information about users in the current site.

**Example Request**

GET /api/v1/site_users/

**Response Values**

If the request is successful, the request returns an HTTP 200 "OK" response.

The HTTP 200 response has the following values.

* list of users in the current site
"""
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [BearerAuthentication, SessionAuthentication]

def list(self, request, *args, **kwargs):
"""
Returns a list of users in the current site
"""
sub_org = EdlySubOrganization.objects.filter(slug=request.GET.get('sub_org', ''))
# sub_org = EdlySubOrganization.objects.filter(slug='test100')
if not sub_org.exists():
return Response({"error": "Sub Organization not found", 'results':[]}, status=400)

sub_org = sub_org.first()
users = get_users_for_site(sub_org).filter(site_count=1)
return Response({'results':users, 'success':True}, status=200)


class EdlySiteDeletionViewSet(viewsets.ViewSet):
"""Deletion of current site and linked user."""
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JwtAuthentication, SessionAuthentication, BearerAuthentication]

def post(self, request):
"""
POST /api/v1/delete_site/
"""
try:
site = get_current_site(request)
call_command('delete_cloud_site', site=site.domain)
return Response({'success':'LMS site deletion was successful'}, status=200)
except Exception as e:
return Response({'error':str(e), 'success':False}, status=400)
116 changes: 116 additions & 0 deletions openedx/features/edly/management/commands/delete_cloud_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
A management command to update formus sites
"""
# pylint: disable=broad-except

import logging

from django.db import transaction
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.contrib.sites.models import Site

from cms.djangoapps.contentstore.management.commands.delete_course import (
Command as DeleteCourseCommand
)
from openedx.features.edly.models import EdlySubOrganization
from figures.sites import get_course_keys_for_site
from openedx.features.edly.api.v1.helper import get_users_for_site

logger = logging.getLogger(__name__)


class Command(BaseCommand):
"""
Custom management command for the deletion of a multisite.
"""
help = 'Update Forums Sites'

def add_arguments(self, parser):
"""
Sending --apply argument with management command will also update the database,
Otherwise It's generate report only.
"""
parser.add_argument(
'--site',
'-a',
default='',
help='Name of the site.',
)

def _delete_users(self, user_obj):
"""
Delete users using the existing edx-platform command.
"""
for user in user_obj:
if user.get('site_count', 1) < 2:
username = user.get('user__username', '')
email = user.get('user__email', '')
try:
# call_command(
# 'manage_user',
# username,
# email,
# '--remove',
# )
self.stderr.write(f"Successfully deleted user: {user.get('user__id', '')}, {email}")
except Exception as e:
logger.error(f"Failed to delete user {username}: {str(e)}")

def _delete_courses(self, site):
"""
Delete courses using the imported delete_course command.
"""
course_ids = get_course_keys_for_site(site)
delete_course_cmd = DeleteCourseCommand()
delete_course_cmd.stdout = self.stdout

for course_id in course_ids:
try:
delete_course_cmd.handle(
course_key=str(course_id),
force=True,
remove_assets=True,
keep_instructors=False
)
logger.info(f"Successfully deleted course: {course_id}")
except Exception as e:
logger.info(f"Failed to delete course {course_id}: {str(e)}")

def delete_django_sub_org(self, sub_org):
"""
Delete the Django sub organization.
"""
count = EdlySubOrganization.objects.filter(edly_organization=sub_org.edly_organization).count()
if count == 1:
sub_org.edly_organization.delete()

sub_org.lms_site.delete()
sub_org.studio_site.delete()
sub_org.preview_site.delete()
sub_org.edx_organization.delete()
sub_org.delete()

def _delete_site_data(self, site, edly_sub_org):
"""
Delete all the site data for a give site configuration.
"""
with transaction.atomic():
self._delete_courses(site)
user_obj = get_users_for_site(edly_sub_org)
self._delete_users(user_obj)
self.delete_django_sub_org(edly_sub_org)

def handle(self, *args, **options):
"""Deletion of site, that are passed from the site"""
try:
site = options.get('site')
site = Site.objects.get(domain=site)
sub_org = EdlySubOrganization.objects.get(lms_site_id=site.id)
if not sub_org:
raise CommandError(f"sub-organzation not found: {site}")

self._delete_site_data(site, sub_org)

except Exception as e:
raise CommandError(f"Failed to delete site: {str(e)}")