diff --git a/openedx/core/djangoapps/appsembler/sites/api.py b/openedx/core/djangoapps/appsembler/sites/api.py index dc3b776f1460..dae005ae4911 100644 --- a/openedx/core/djangoapps/appsembler/sites/api.py +++ b/openedx/core/djangoapps/appsembler/sites/api.py @@ -67,7 +67,7 @@ def get_serializer_class(self): return super(SiteConfigurationViewSet, self).get_serializer_class() def perform_destroy(self, instance): - delete_site(instance) + delete_site(instance.site) class FileUploadView(views.APIView): 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 f647e9520b05..64214d103a71 100644 --- a/openedx/core/djangoapps/appsembler/sites/urls.py +++ b/openedx/core/djangoapps/appsembler/sites/urls.py @@ -1,6 +1,7 @@ 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, 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()