From 51cf14cfeb8fd9135dc20150b9b355df00ed2018 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb Date: Wed, 5 May 2021 15:39:27 +0500 Subject: [PATCH] Added credentials site creation API EDLY-2904 (#8) --- .../edly_credentials_app/api/permissions.py | 14 +++ .../edly_credentials_app/api/v1/constants.py | 22 +++++ .../edly_credentials_app/api/v1/urls.py | 8 +- .../api/v1/views/edly_sites.py | 80 +++++++++++++++ .../api/v1/views/tests/test_views.py | 99 ++++++++++++++++++- .../edly_credentials_app/helpers.py | 60 +++++++++++ .../tests/test_helpers.py | 87 ++++++++++++++++ credentials/urls.py | 2 +- 8 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/constants.py create mode 100644 credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/edly_sites.py create mode 100644 credentials/apps/edx_credentials_extensions/edly_credentials_app/helpers.py create mode 100644 credentials/apps/edx_credentials_extensions/edly_credentials_app/tests/test_helpers.py diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/permissions.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/permissions.py index b82bf746d..bd07c39db 100644 --- a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/permissions.py +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/permissions.py @@ -5,6 +5,7 @@ from rest_framework import permissions +from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.constants import EDLY_PANEL_WORKER_USER from credentials.apps.edx_credentials_extensions.edly_credentials_app.utils import user_has_edx_organization_access logger = logging.getLogger(__name__) @@ -23,3 +24,16 @@ def has_permission(self, request, view): return True return False + + +class CanAccessSiteCreation(permissions.BasePermission): + """ + Checks if a user has the access to create and update methods for sites. + """ + + def has_permission(self, request, view): + """ + Checks for user's permission for current site. + """ + logger.info('User {user} is trying to access site creation API'.format(user=request.user.username)) + return request.user.is_staff or request.user.username == EDLY_PANEL_WORKER_USER diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/constants.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/constants.py new file mode 100644 index 000000000..5ed9d1779 --- /dev/null +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/constants.py @@ -0,0 +1,22 @@ +""" +Constants for Edly Credentials API. +""" +from django.utils.translation import ugettext as _ + +ERROR_MESSAGES = { + 'CLIENT_SITES_SETUP_SUCCESS': _('Client sites setup successful.'), + 'CLIENT_SITES_SETUP_FAILURE': _('Client sites setup failed.'), +} + +CLIENT_SITE_SETUP_FIELDS = [ + 'lms_site', + 'credentials_site', + 'wordpress_site', + 'edly_slug', + 'platform_name', + 'discovery_site', + 'theme_dir_name', + 'oauth_clients' +] + +EDLY_PANEL_WORKER_USER = 'edly_panel_worker' diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/urls.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/urls.py index a5887f8f1..f1e1d53bb 100644 --- a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/urls.py +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/urls.py @@ -1,9 +1,15 @@ +from django.conf.urls import url from rest_framework import routers +from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.views.edly_sites import EdlySiteViewSet from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.views.program_certificate_configuration import ProgramCertificateConfigurationViewSet router = routers.SimpleRouter() router.register(r'program-certificate-configuration', ProgramCertificateConfigurationViewSet, basename='program-certificate-configuration') -urlpatterns = router.urls +urlpatterns = [ + url(r'^edly_sites/', EdlySiteViewSet.as_view(), name='edly_sites'), +] + +urlpatterns += router.urls diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/edly_sites.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/edly_sites.py new file mode 100644 index 000000000..6176f391d --- /dev/null +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/edly_sites.py @@ -0,0 +1,80 @@ +""" +Views for Edly Site Creation API. +""" +from django.contrib.sites.models import Site +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from credentials.apps.core.models import SiteConfiguration +from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.permissions import CanAccessSiteCreation +from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.constants import ERROR_MESSAGES +from credentials.apps.edx_credentials_extensions.edly_credentials_app.helpers import ( + get_credentials_site_configuration, + validate_site_configurations, +) + + +class EdlySiteViewSet(APIView): + """ + Creates credentials site and it's site configuration. + """ + permission_classes = [IsAuthenticated, CanAccessSiteCreation] + + def post(self, request): + """ + POST /api/edly_api/v1/edly_sites. + """ + validations_messages = validate_site_configurations(request.data) + if len(validations_messages) > 0: + return Response(validations_messages, status=status.HTTP_400_BAD_REQUEST) + + try: + self.process_client_sites_setup() + return Response( + {'success': ERROR_MESSAGES.get('CLIENT_SITES_SETUP_SUCCESS')}, + status=status.HTTP_200_OK + ) + except TypeError: + return Response( + {'error': ERROR_MESSAGES.get('CLIENT_SITES_SETUP_FAILURE')}, + status=status.HTTP_400_BAD_REQUEST + ) + + def process_client_sites_setup(self): + """ + Process client sites setup and update configurations. + """ + edly_slug = self.request.data.get('edly_slug', '') + credentials_base = self.request.data.get('credentials_site', '') + theme_dir_name = self.request.data.get('theme_dir_name', 'openedx') + lms_url_root = '{protocol}://{lms_url_root}'.format( + protocol=self.request.data.get('protocol', 'https'), + lms_url_root=self.request.data.get('lms_site', '') + ) + catalog_api_url = '{protocol}://{discovery_site}/api/v1/'.format( + protocol=self.request.data.get('protocol', 'https'), + discovery_site=self.request.data.get('discovery_site', '') + ) + wordpress_site = '{protocol}://{wordpress_site}'.format( + protocol=self.request.data.get('protocol', 'https'), + wordpress_site=self.request.data.get('wordpress_site', '') + ) + credentials_site, __ = Site.objects.update_or_create(domain=credentials_base, defaults=dict(name=credentials_base)) + credentials_site_config, __ = SiteConfiguration.objects.update_or_create( + site=credentials_site, + defaults=dict( + edx_org_short_name=edly_slug, + platform_name=self.request.data.get('platform_name', ''), + company_name=self.request.data.get('platform_name', ''), + theme_name=theme_dir_name, + lms_url_root=lms_url_root, + catalog_api_url=catalog_api_url, + tos_url='{lms_url_root}/tos'.format(lms_url_root=lms_url_root), + privacy_policy_url='{lms_url_root}/privacy'.format(lms_url_root=lms_url_root), + homepage_url=wordpress_site, + certificate_help_url='{wordpress_site}/contact-us'.format(wordpress_site=wordpress_site), + edly_client_branding_and_django_settings=get_credentials_site_configuration(self.request.data), + ) + ) diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/tests/test_views.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/tests/test_views.py index cc998aa89..764960271 100644 --- a/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/tests/test_views.py +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/api/v1/views/tests/test_views.py @@ -3,14 +3,20 @@ """ import json +from django.contrib.sites.models import Site from django.urls import reverse - +from rest_framework import status from rest_framework.test import APITestCase from credentials.apps.catalog.tests.factories import ProgramFactory +from credentials.apps.core.models import SiteConfiguration from credentials.apps.core.tests.factories import UserFactory from credentials.apps.core.tests.mixins import SiteMixin from credentials.apps.credentials.tests.factories import ProgramCertificateFactory +from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.constants import ( + CLIENT_SITE_SETUP_FIELDS, + EDLY_PANEL_WORKER_USER, +) JSON_CONTENT_TYPE = 'application/json' @@ -99,3 +105,94 @@ def test_list_program_certificate_configuration(self): actual_data = response.json() actual_data = actual_data.get('results')[0] assert str(program_certificate_configuration.program_uuid) == actual_data.get('program_uuid') + + +class EdlySiteViewSet(APITestCase): + """ + Unit tests for EdlySiteViewSet viewset. + """ + + def setUp(self): + """ + Prepare environment for tests. + """ + super(EdlySiteViewSet, self).setUp() + self.admin_user = UserFactory(is_staff=True, username=EDLY_PANEL_WORKER_USER) + self.edly_sites_url = reverse('edly_api:edly_sites') + self.client.force_authenticate(self.admin_user) + self.request_data = dict( + lms_site='example.lms', + credentials_site='example.credentials', + wordpress_site='example.wordpress', + edly_slug='edx', + platform_name='Edly', + discovery_site='example.discovery', + theme_dir_name='openedx', + oauth_clients={ + 'credentials-sso': { + 'id': 'credentials-sso-key', + 'secret': 'credentials-sso-secret' + }, + 'credentials-backend': { + 'id': 'credentials-backend-key', + 'secret': 'credentials-backend-secret' + } + }, + ) + + def test_without_authentication(self): + """ + Verify authentication is required when accessing the endpoint. + """ + self.client.logout() + response = self.client.post(self.edly_sites_url) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_without_permission(self): + """ + Verify panel permission is required when accessing the endpoint. + """ + user = UserFactory() + self.client.logout() + self.client.force_authenticate(user) + response = self.client.post(self.edly_sites_url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_request_data_validation(self): + """ + Verify validation messages in response for missing required data. + """ + response = self.client.post(self.edly_sites_url, data={}) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert set(response.json().keys()) == set(CLIENT_SITE_SETUP_FIELDS) + + def test_client_setup(self): + """ + Verify successful client setup with correct data. + """ + response = self.client.post(self.edly_sites_url, data=self.request_data, format='json') + + assert response.status_code == status.HTTP_200_OK + credentials_site = Site.objects.get(domain=self.request_data.get('credentials_site', '')) + assert credentials_site.siteconfiguration + assert credentials_site.siteconfiguration.edly_client_branding_and_django_settings + + def test_client_setup_idempotent(self): + """ + Test that the values are only update not created on multiple API calls. + """ + response = self.client.post(self.edly_sites_url, data=self.request_data, format='json') + + assert response.status_code == status.HTTP_200_OK + credentials_site = Site.objects.get(domain=self.request_data.get('credentials_site', '')) + assert credentials_site.siteconfiguration + + sites_count = Site.objects.all().count() + site_configurations_count = SiteConfiguration.objects.all().count() + response = self.client.post(self.edly_sites_url, data=self.request_data, format='json') + + assert response.status_code == status.HTTP_200_OK + assert Site.objects.all().count() == sites_count + assert SiteConfiguration.objects.all().count() == site_configurations_count diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/helpers.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/helpers.py new file mode 100644 index 000000000..c797fbf49 --- /dev/null +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/helpers.py @@ -0,0 +1,60 @@ +from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.constants import CLIENT_SITE_SETUP_FIELDS + + +def validate_site_configurations(request_data): + """ + Identify missing required fields for client's site setup. + + Arguments: + request_data (dict): Request data passed for site setup + + Returns: + validation_messages (dict): Missing fields information + """ + + validation_messages = {} + + for field in CLIENT_SITE_SETUP_FIELDS: + if not request_data.get(field, None): + validation_messages[field] = '{0} is Missing'.format(field.replace('_', ' ').title()) + + return validation_messages + + +def get_credentials_site_configuration(request_data): + """ + Prepare Credentials Site Configurations for Client based on Request Data. + + Arguments: + request_data (dict): Request data passed for site setup + + Returns: + (dict): Credentials Site Configuration + """ + protocol = request_data.get('protocol', 'https') + lms_site = request_data.get('lms_site', '') + lms_site_with_protocol = '{protocol}://{lms_root_domain}'.format( + protocol=protocol, + lms_root_domain=lms_site, + ) + oauth2_clients = request_data.get('oauth_clients', {}) + credentials_sso_values = oauth2_clients.get('credentials-sso', {}) + credentials_backend_values = oauth2_clients.get('credentials-backend', {}) + + return { + 'DJANGO_SETTINGS_OVERRIDE': { + 'SOCIAL_AUTH_EDX_OAUTH2_KEY': credentials_sso_values.get('id', ''), + 'SOCIAL_AUTH_EDX_OAUTH2_SECRET': credentials_sso_values.get('secret', ''), + 'SOCIAL_AUTH_EDX_OAUTH2_ISSUER': lms_site_with_protocol, + 'SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT': lms_site_with_protocol, + 'SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT': lms_site_with_protocol, + 'SOCIAL_AUTH_EDX_OAUTH2_LOGOUT_URL': '{lms_site_with_protocol}/logout'.format( + lms_site_with_protocol=lms_site_with_protocol + ), + 'BACKEND_SERVICE_EDX_OAUTH2_KEY': credentials_backend_values.get('id', ''), + 'BACKEND_SERVICE_EDX_OAUTH2_SECRET': credentials_backend_values.get('secret', ''), + 'BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL': '{lms_site_with_protocol}/oauth2'.format( + lms_site_with_protocol=lms_site_with_protocol + ), + } + } diff --git a/credentials/apps/edx_credentials_extensions/edly_credentials_app/tests/test_helpers.py b/credentials/apps/edx_credentials_extensions/edly_credentials_app/tests/test_helpers.py new file mode 100644 index 000000000..f9ad87c3c --- /dev/null +++ b/credentials/apps/edx_credentials_extensions/edly_credentials_app/tests/test_helpers.py @@ -0,0 +1,87 @@ +import pytest +from django.test import RequestFactory, TestCase + +from credentials.apps.core.tests.factories import SiteFactory +from credentials.apps.edx_credentials_extensions.edly_credentials_app.helpers import ( + get_credentials_site_configuration, + validate_site_configurations, +) + + +@pytest.mark.django_db +class EdlyAppHelperMethodsTests(TestCase): + """ + Unit tests for helper methods. + """ + + def setUp(self): + super(EdlyAppHelperMethodsTests, self).setUp() + self.request = RequestFactory().get('/') + self.request.site = SiteFactory() + self.request_data = dict( + lms_site='example.lms', + credentials_site='example.credentials', + wordpress_site='example.wordpress', + edly_slug='edx', + platform_name='Edly', + discovery_site='example.discovery', + theme_dir_name='openedx', + oauth_clients={ + 'credentials-sso': { + 'id': 'credentials-sso-key', + 'secret': 'credentials-sso-secret' + }, + 'credentials-backend': { + 'id': 'credentials-backend-key', + 'secret': 'credentials-backend-secret' + } + }, + ) + + def test_validate_site_configurations(self): + """ + Test that required site creation data is present in request data. + """ + lms_site = self.request_data.pop('lms_site') + validation_messages = validate_site_configurations(self.request_data) + expected_message = 'Lms Site is Missing' + assert validation_messages.get('lms_site') == expected_message + + self.request_data['lms_site'] = lms_site + validation_messages = validate_site_configurations(self.request_data) + assert not validation_messages + + def test_get_credentials_site_configuration(self): + """ + Test that correct credentials site configuration data is returned using the request data. + """ + protocol = self.request_data.get('protocol', 'https') + lms_site = self.request_data.get('lms_site', '') + lms_site_with_protocol = '{protocol}://{lms_root_domain}'.format( + protocol=protocol, + lms_root_domain=lms_site, + ) + oauth2_clients = self.request_data.get('oauth_clients', {}) + credentials_sso_values = oauth2_clients.get('credentials-sso', {}) + credentials_backend_values = oauth2_clients.get('credentials-backend', {}) + expected_site_configuration = { + 'DJANGO_SETTINGS_OVERRIDE': { + 'SOCIAL_AUTH_EDX_OAUTH2_KEY': credentials_sso_values.get('id', ''), + 'SOCIAL_AUTH_EDX_OAUTH2_SECRET': credentials_sso_values.get('secret', ''), + 'SOCIAL_AUTH_EDX_OAUTH2_ISSUER': lms_site_with_protocol, + 'SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT': lms_site_with_protocol, + 'SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT': lms_site_with_protocol, + 'SOCIAL_AUTH_EDX_OAUTH2_LOGOUT_URL': '{lms_site_with_protocol}/logout'.format( + lms_site_with_protocol=lms_site_with_protocol + ), + 'BACKEND_SERVICE_EDX_OAUTH2_KEY': credentials_backend_values.get('id', ''), + 'BACKEND_SERVICE_EDX_OAUTH2_SECRET': credentials_backend_values.get('secret', ''), + 'BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL': '{lms_site_with_protocol}/oauth2'.format( + lms_site_with_protocol=lms_site_with_protocol + ), + } + } + + credentials_site_configuration = get_credentials_site_configuration(self.request_data) + for key, value in credentials_site_configuration.items(): + assert expected_site_configuration[key] == value diff --git a/credentials/urls.py b/credentials/urls.py index 5b282c8de..11bfc6d9d 100644 --- a/credentials/urls.py +++ b/credentials/urls.py @@ -52,7 +52,7 @@ # Edly Urls urlpatterns += [ - url(r'^edly-api/', include(('credentials.apps.edx_credentials_extensions.edly_credentials_app.urls', 'edly_credentials_app'), namespace='edly_api')), + url(r'^edly_api/', include(('credentials.apps.edx_credentials_extensions.edly_credentials_app.urls', 'edly_credentials_app'), namespace='edly_api')), ] handler500 = 'credentials.apps.core.views.render_500'