diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index fe8553b679a..921a09967a1 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -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 @@ -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']: diff --git a/openedx/features/edly/api/v1/helper.py b/openedx/features/edly/api/v1/helper.py new file mode 100644 index 00000000000..b8b2929be16 --- /dev/null +++ b/openedx/features/edly/api/v1/helper.py @@ -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 diff --git a/openedx/features/edly/api/v1/urls.py b/openedx/features/edly/api/v1/urls.py index 543df19f17e..42630c3ab10 100644 --- a/openedx/features/edly/api/v1/urls.py +++ b/openedx/features/edly/api/v1/urls.py @@ -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', @@ -29,3 +35,6 @@ ) urlpatterns = router.urls +urlpatterns += [ + url(r'^delete_site/', EdlySiteDeletionViewSet.as_view({'post': 'post'}), name='delete_site') +] diff --git a/openedx/features/edly/api/v1/views/user_mutisites.py b/openedx/features/edly/api/v1/views/user_mutisites.py index 6f5448def75..749c0558005 100644 --- a/openedx/features/edly/api/v1/views/user_mutisites.py +++ b/openedx/features/edly/api/v1/views/user_mutisites.py @@ -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): @@ -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) diff --git a/openedx/features/edly/management/commands/delete_cloud_site.py b/openedx/features/edly/management/commands/delete_cloud_site.py new file mode 100644 index 00000000000..c7fe56611b1 --- /dev/null +++ b/openedx/features/edly/management/commands/delete_cloud_site.py @@ -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)}")