From a8853dd47e74e1c9637a61e6ec580ce92012ea99 Mon Sep 17 00:00:00 2001 From: Ahmed Jazzar Date: Tue, 9 Jul 2019 11:16:20 -0700 Subject: [PATCH 1/2] Working delete site function --- openedx/core/djangoapps/appsembler/sites/api.py | 11 +++++++++++ openedx/core/djangoapps/appsembler/sites/urls.py | 3 +++ openedx/core/djangoapps/appsembler/sites/utils.py | 6 +++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/appsembler/sites/api.py b/openedx/core/djangoapps/appsembler/sites/api.py index dc3b776f1460..66a168f9c397 100644 --- a/openedx/core/djangoapps/appsembler/sites/api.py +++ b/openedx/core/djangoapps/appsembler/sites/api.py @@ -66,6 +66,17 @@ def get_serializer_class(self): return self.create_serializer_class return super(SiteConfigurationViewSet, self).get_serializer_class() + def perform_destroy(self, instance): + delete_site(instance.site) + + +class OffboardOrganizationAPIView(generics.DestroyAPIView): + authentication_classes = (OAuth2AuthenticationAllowInactiveUser,) + permission_classes = (IsAuthenticated, AMCAdminPermission) + queryset = Site.objects.all() + serializer_class = SiteSerializer + lookup_field = 'domain' + def perform_destroy(self, instance): delete_site(instance) diff --git a/openedx/core/djangoapps/appsembler/sites/urls.py b/openedx/core/djangoapps/appsembler/sites/urls.py index f647e9520b05..75da6678045d 100644 --- a/openedx/core/djangoapps/appsembler/sites/urls.py +++ b/openedx/core/djangoapps/appsembler/sites/urls.py @@ -1,12 +1,14 @@ from django.conf import settings from django.conf.urls import url, include from rest_framework.routers import DefaultRouter + from openedx.core.djangoapps.appsembler.sites.api import ( CustomDomainView, DomainAvailabilityView, DomainSwitchView, HostFilesView, FileUploadView, + OffboardOrganizationAPIView, SiteConfigurationViewSet, SiteCreateView, SiteViewSet, @@ -27,6 +29,7 @@ url(r'^custom_domain/', CustomDomainView.as_view()), url(r'^domain_switch/', DomainSwitchView.as_view()), url(r'^register/', SiteCreateView.as_view()), + url(r'^offboard/(?P\w+(\.\w+)*(:[0-9]+)?\/?)/', OffboardOrganizationAPIView.as_view()), url(r'^', include(router.urls)), ] diff --git a/openedx/core/djangoapps/appsembler/sites/utils.py b/openedx/core/djangoapps/appsembler/sites/utils.py index 3dd172e435d5..bc26710e7e57 100644 --- a/openedx/core/djangoapps/appsembler/sites/utils.py +++ b/openedx/core/djangoapps/appsembler/sites/utils.py @@ -306,10 +306,10 @@ def bootstrap_site(site, org_data=None, user_email=None): return organization, site, user -def delete_site(site_id): - site = Site.objects.get(id=site_id) +def delete_site(site): site.configuration.delete() - site.themes.delete() + site.themes.all().delete() + site.delete() From 68fb3500d37abcd94602631030fe4a5cc25b5cbe Mon Sep 17 00:00:00 2001 From: Ahmed Jazzar Date: Thu, 19 Mar 2020 10:52:33 +0000 Subject: [PATCH 2/2] Remove Site management command Update docs Fix tests Updating tests Remove the white space Check for theme count Renaming --- .../core/djangoapps/appsembler/sites/api.py | 11 ----- .../sites/management/commands/remove_site.py | 41 +++++++++++++++++ .../appsembler/sites/tests/test_commands.py | 46 +++++++++++++++++++ .../core/djangoapps/appsembler/sites/urls.py | 2 - 4 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 openedx/core/djangoapps/appsembler/sites/management/commands/remove_site.py diff --git a/openedx/core/djangoapps/appsembler/sites/api.py b/openedx/core/djangoapps/appsembler/sites/api.py index 66a168f9c397..dae005ae4911 100644 --- a/openedx/core/djangoapps/appsembler/sites/api.py +++ b/openedx/core/djangoapps/appsembler/sites/api.py @@ -70,17 +70,6 @@ def perform_destroy(self, instance): delete_site(instance.site) -class OffboardOrganizationAPIView(generics.DestroyAPIView): - authentication_classes = (OAuth2AuthenticationAllowInactiveUser,) - permission_classes = (IsAuthenticated, AMCAdminPermission) - queryset = Site.objects.all() - serializer_class = SiteSerializer - lookup_field = 'domain' - - def perform_destroy(self, instance): - delete_site(instance) - - class FileUploadView(views.APIView): parser_classes = (MultiPartParser,) # TODO: oauth token isn't present after step 3 in signup, fix later diff --git a/openedx/core/djangoapps/appsembler/sites/management/commands/remove_site.py b/openedx/core/djangoapps/appsembler/sites/management/commands/remove_site.py new file mode 100644 index 000000000000..81caac89edf9 --- /dev/null +++ b/openedx/core/djangoapps/appsembler/sites/management/commands/remove_site.py @@ -0,0 +1,41 @@ +from django.core.management.base import BaseCommand, CommandError +from django.contrib.sites.models import Site + +from openedx.core.djangoapps.appsembler.sites.utils import delete_site + + +class Command(BaseCommand): + """ + Remove a Tahoe website from LMS records. + + Must be used `remove_site` on AMC to avoid any errors there. + """ + + def add_arguments(self, parser): + parser.add_argument( + 'domain', + help='The domain of the organization to be deleted.', + type=str, + ) + + def handle(self, *args, **options): + organization_domain = options['domain'] + self.stdout.write(self.style.WARNING('Same command must be ran on the connected AMC instance')) + + self.stdout.write('Removing "%s" in progress...' % organization_domain) + organization = self._get_site(organization_domain) + + delete_site(organization) + self.stdout.write(self.style.SUCCESS('Successfully removed site "%s"' % organization_domain)) + + def _get_site(self, domain): + """ + Locates the site to be deleted and return its instance. + + :param domain: The domain of the site to be returned. + :return: Returns the site object that has the given domain. + """ + try: + return Site.objects.get(domain=domain) + except Site.DoesNotExist: + raise CommandError('Cannot find "%s" in Sites!' % domain) diff --git a/openedx/core/djangoapps/appsembler/sites/tests/test_commands.py b/openedx/core/djangoapps/appsembler/sites/tests/test_commands.py index 0ed21b9e53e2..f5faa54191ea 100644 --- a/openedx/core/djangoapps/appsembler/sites/tests/test_commands.py +++ b/openedx/core/djangoapps/appsembler/sites/tests/test_commands.py @@ -8,6 +8,8 @@ from django.core.management import call_command from openedx.core.djangoapps.appsembler.sites.management.commands.create_devstack_site import Command +from openedx.core.djangoapps.site_configuration.models import SiteConfiguration +from openedx.core.djangoapps.theming.models import SiteTheme from organizations.models import Organization from provider.constants import CONFIDENTIAL from provider.oauth2.models import AccessToken, RefreshToken, Client @@ -68,3 +70,47 @@ def test_create_devstack_site(self): assert fake_token == '80bfa968ffad007c79bfc603f3670c99', 'Ensure hash is identical to AMC' assert AccessToken.objects.get(user=user).token == fake_token, 'Access token is needed' assert RefreshToken.objects.get(user=user).token == fake_token, 'Refresh token is needed' + + +@override_settings( + DEBUG=True, + DEFAULT_SITE_THEME='edx-theme-codebase', + FEATURES={ + 'AMC_APP_URL': 'http://localhost:13000', + "DISABLE_COURSE_CREATION": False, + "ENABLE_CREATOR_GROUP": True, + }, + COMPREHENSIVE_THEME_DIRS=[settings.REPO_ROOT / 'common/test/appsembler'], +) +class RemoveSiteCommandTestCase(TestCase): + """ + Test ./manage.py lms remove_site mysite + """ + def setUp(self): + assert settings.ENABLE_COMPREHENSIVE_THEMING + Client.objects.create(url=settings.FEATURES['AMC_APP_URL'], client_type=CONFIDENTIAL) + + self.to_be_deleted = 'delete' + self.shall_remain = 'keep' + + # This command should be tested above + call_command('create_devstack_site', self.to_be_deleted) + call_command('create_devstack_site', self.shall_remain) + + def test_create_devstack_site(self): + """ + Test that `create_devstack_site` and creates the required objects. + """ + call_command('remove_site', '{}.localhost:18000'.format(self.to_be_deleted)) + + # Ensure objects are removed correctly. + deleted_domain = '{}.localhost:18000'.format(self.to_be_deleted) + remained_domain = '{}.localhost:18000'.format(self.shall_remain) + + assert not Site.objects.filter(domain=deleted_domain).exists() + site = Site.objects.get(domain=remained_domain) + + assert SiteConfiguration.objects.count() == 1 + assert SiteConfiguration.objects.get(site=site) + + assert SiteTheme.objects.filter(site=site).count() == site.themes.count() diff --git a/openedx/core/djangoapps/appsembler/sites/urls.py b/openedx/core/djangoapps/appsembler/sites/urls.py index 75da6678045d..64214d103a71 100644 --- a/openedx/core/djangoapps/appsembler/sites/urls.py +++ b/openedx/core/djangoapps/appsembler/sites/urls.py @@ -8,7 +8,6 @@ DomainSwitchView, HostFilesView, FileUploadView, - OffboardOrganizationAPIView, SiteConfigurationViewSet, SiteCreateView, SiteViewSet, @@ -29,7 +28,6 @@ url(r'^custom_domain/', CustomDomainView.as_view()), url(r'^domain_switch/', DomainSwitchView.as_view()), url(r'^register/', SiteCreateView.as_view()), - url(r'^offboard/(?P\w+(\.\w+)*(:[0-9]+)?\/?)/', OffboardOrganizationAPIView.as_view()), url(r'^', include(router.urls)), ]