diff --git a/course_discovery/apps/edly_discovery_app/__init__.py b/course_discovery/apps/edly_discovery_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/course_discovery/apps/edly_discovery_app/admin.py b/course_discovery/apps/edly_discovery_app/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/course_discovery/apps/edly_discovery_app/api/v1/__init__.py b/course_discovery/apps/edly_discovery_app/api/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/course_discovery/apps/edly_discovery_app/api/v1/constants.py b/course_discovery/apps/edly_discovery_app/api/v1/constants.py new file mode 100644 index 0000000000..000146fc1b --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/api/v1/constants.py @@ -0,0 +1,19 @@ +""" +Constants for Edly Sites 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', + 'cms_site', + 'discovery_site', + 'payments_site', + 'wordpress_site', + 'partner_name', + 'partner_short_code', +] diff --git a/course_discovery/apps/edly_discovery_app/api/v1/helpers.py b/course_discovery/apps/edly_discovery_app/api/v1/helpers.py new file mode 100644 index 0000000000..b409cc79c5 --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/api/v1/helpers.py @@ -0,0 +1,29 @@ +""" +Helper methods for Edly Sites API. +""" +import logging + +from edly_discovery_app.api.v1.constants import CLIENT_SITE_SETUP_FIELDS + +logger = logging.getLogger(__name__) + + +def validate_partner_configurations(request_data): + """ + Identify missing required fields for client's site partner 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): + field_title = field.replace('_', ' ').title() # Convert data field 'partner_name' to 'Partner Name' + validation_messages[field] = '{field_title} is Missing'.format(field_title=field_title) + + return validation_messages diff --git a/course_discovery/apps/edly_discovery_app/api/v1/permissions.py b/course_discovery/apps/edly_discovery_app/api/v1/permissions.py new file mode 100644 index 0000000000..97d81b840d --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/api/v1/permissions.py @@ -0,0 +1,21 @@ +""" +Permissions for Edly Sites API. +""" +import logging + +from django.conf import settings +from rest_framework import permissions + +logger = logging.getLogger(__name__) + + +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. + """ + return request.user.is_staff or request.user.username == settings.EDLY_PANEL_WORKER_USER diff --git a/course_discovery/apps/edly_discovery_app/api/v1/tests/__init__.py b/course_discovery/apps/edly_discovery_app/api/v1/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/course_discovery/apps/edly_discovery_app/api/v1/tests/test_views.py b/course_discovery/apps/edly_discovery_app/api/v1/tests/test_views.py new file mode 100644 index 0000000000..1fbf0345ad --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/api/v1/tests/test_views.py @@ -0,0 +1,120 @@ +""" +Unit tests for Edly Site API views. +""" +from unittest import TestCase +import pytest + +from django.conf import settings +from django.contrib.sites.models import Site +from django.test.client import Client, RequestFactory +from django.urls import reverse +from rest_framework import status + +from course_discovery.apps.core.models import Partner +from course_discovery.apps.core.tests.factories import ( + SiteFactory, + UserFactory, + PartnerFactory, + USER_PASSWORD, +) +from edly_discovery_app.api.v1.constants import CLIENT_SITE_SETUP_FIELDS + +pytestmark = pytest.mark.django_db + + +def get_request_object_with_partner_access(user=None, short_code=None, site=None): + """ + Get request object with partner access. + + Arguments: + user (User): Django User object + short_code (String): Partner Short Code + site (Site): Django Site object + + Returns: + Request: WSGI Request object with partner access + """ + request_site = SiteFactory() if not site else site + PartnerFactory(short_code=short_code, site=request_site) + request = RequestFactory().get('/') + request.site = request_site + + if user: + request.user = user + + return request + + +class EdlySiteViewSet(TestCase): + """ + Unit tests for EdlySiteViewSet View. + """ + + def setUp(self): + """ + Setup data for test cases. + """ + self.user = UserFactory(username=settings.EDLY_PANEL_WORKER_USER) + self.short_code = 'red' + self.url = reverse('edly_discovery_app:v1:edly_sites') + self.request = get_request_object_with_partner_access(self.user, short_code=self.short_code) + self.client = Client(SERVER_NAME=self.request.site.domain) + self.client.login(username=self.user.username, password=USER_PASSWORD) + + def test_without_authentication(self): + """ + Verify authentication is required when accessing the endpoint. + """ + self.client.logout() + response = self.client.post(self.url) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_without_permission(self): + """ + Verify permission is required when accessing the endpoint. + """ + edly_panel_user = UserFactory() + api_url = reverse('edly_discovery_app:v1:edly_sites') + request = get_request_object_with_partner_access(edly_panel_user, short_code='test_org') + + client = Client(SERVER_NAME=request.site.domain) + client.login(username=edly_panel_user.username, password=USER_PASSWORD) + + response = client.post(api_url) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_request_data_authentication(self): + """ + Verify authentication for request data. + """ + response = self.client.post(self.url) + request_data_fields_validations = [*response.json().keys()] + + assert set(request_data_fields_validations) == set(CLIENT_SITE_SETUP_FIELDS) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_client_setup(self): + """ + Verify authentication for request data. + """ + expected_request_data = { + 'protocol': 'http', + 'partner_name': 'Test', + 'partner_short_code': 'test', + 'lms_site': 'test.edx.devstack.lms:18000', + 'cms_site': 'test.edx.devstack.lms:18010', + 'discovery_site': 'test.edx.devstack.lms:18381', + 'payments_site': 'test.edx.devstack.lms:18130', + 'wordpress_site': 'test.wordpress.edx.devstack.lms', + } + + response = self.client.post(self.url, data=expected_request_data) + assert response.status_code == status.HTTP_200_OK + + discovery_site = Site.objects.get( + domain=expected_request_data['discovery_site'], + name=expected_request_data['discovery_site'], + ) + partner = Partner.objects.get(site=discovery_site) + assert discovery_site.domain == expected_request_data['discovery_site'] + assert partner.short_code == expected_request_data['partner_short_code'] diff --git a/course_discovery/apps/edly_discovery_app/api/v1/urls.py b/course_discovery/apps/edly_discovery_app/api/v1/urls.py new file mode 100644 index 0000000000..d413265189 --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/api/v1/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import include, url + +from edly_discovery_app.api.v1 import views + + +app_name = 'v1' +urlpatterns = [ + url(r'^edly_sites/', views.EdlySiteViewSet.as_view(), name='edly_sites'), +] diff --git a/course_discovery/apps/edly_discovery_app/api/v1/views.py b/course_discovery/apps/edly_discovery_app/api/v1/views.py new file mode 100644 index 0000000000..7f640fc433 --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/api/v1/views.py @@ -0,0 +1,91 @@ +""" +Views for Edly Sites API. +""" +from django.contrib.sites.models import Site +from rest_framework import status, viewsets, filters +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from course_discovery.apps.core.models import Partner +from edly_discovery_app.api.v1.constants import ERROR_MESSAGES +from edly_discovery_app.api.v1.helpers import validate_partner_configurations +from edly_discovery_app.api.v1.permissions import CanAccessSiteCreation + + +class EdlySiteViewSet(APIView): + """ + Create Default Site and Partner Configuration. + """ + authentication_classes = (SessionAuthentication,) + permission_classes = [IsAuthenticated, CanAccessSiteCreation] + + def post(self, request): + """ + POST /edly_api/v1/edly_sites/ + """ + + validations_messages = validate_partner_configurations(request.data) + if len(validations_messages) > 0: + return Response(validations_messages, status=status.HTTP_400_BAD_REQUEST) + + try: + self.discovery_site_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 discovery_site_setup(self): + """ + Discovery site setup with default partner configurations. + """ + discovery_base = self.request.data.get('discovery_site', '') + discovery_site, __ = Site.objects.get_or_create(domain=discovery_base, name=discovery_base) + return self.get_updated_site_partner(discovery_site) + + def get_updated_site_partner(self, discovery_site): + """ + Get updated site partner based on request data. + """ + protocol = self.request.data.get('protocol', 'https') + lms_base = self.request.data.get('lms_site', '') + wordpress_base = self.request.data.get('wordpress_site', '') + partner, __ = Partner.objects.get_or_create(site=discovery_site) + + partner.name = self.request.data.get('partner_name', '') + partner.short_code = self.request.data.get('partner_short_code', '') + partner.lms_url = '{protocol}://{lms_domain}'.format(protocol=protocol, lms_domain=lms_base) + partner.studio_url = '{protocol}://{cms_domain}'.format( + protocol=protocol, + cms_domain=self.request.data.get('cms_site', ''), + ) + partner.courses_api_url = '{protocol}://{lms_domain}/api/courses/v1/'.format( + protocol=protocol, + lms_domain=lms_base, + ) + partner.ecommerce_api_url = '{protocol}://{payments_domain}/api/v2/'.format( + protocol=protocol, + payments_domain=self.request.data.get('payments_site', ''), + ) + partner.organizations_api_url = '{protocol}://{lms_domain}/api/organizations/v0/organizations/'.format( + protocol=protocol, + lms_domain=lms_base, + ) + partner.marketing_site_url_root = '{protocol}://{wordpress_domain}/'.format( + protocol=protocol, + wordpress_domain=wordpress_base, + ) + partner.marketing_site_api_url = '{protocol}://{wordpress_domain}/wp-json/edly/v1/course_runs'.format( + protocol=protocol, + wordpress_domain=wordpress_base, + ) + partner.save() + + return partner diff --git a/course_discovery/apps/edly_discovery_app/apps.py b/course_discovery/apps/edly_discovery_app/apps.py new file mode 100644 index 0000000000..8bf5bf9dbc --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class EdlyDiscoveryAppConfig(AppConfig): + name = 'edly_discovery_app' diff --git a/course_discovery/apps/edly_discovery_app/migrations/__init__.py b/course_discovery/apps/edly_discovery_app/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/course_discovery/apps/edly_discovery_app/models.py b/course_discovery/apps/edly_discovery_app/models.py new file mode 100644 index 0000000000..71a8362390 --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/course_discovery/apps/edly_discovery_app/tests.py b/course_discovery/apps/edly_discovery_app/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/course_discovery/apps/edly_discovery_app/urls.py b/course_discovery/apps/edly_discovery_app/urls.py new file mode 100644 index 0000000000..610de8fbd8 --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import include, url + +app_name = 'edly_discovery_app' + +urlpatterns = [ + url(r'^v1/', include('course_discovery.apps.edly_discovery_app.api.v1.urls')), +] diff --git a/course_discovery/apps/edly_discovery_app/views.py b/course_discovery/apps/edly_discovery_app/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/course_discovery/apps/edly_discovery_app/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/course_discovery/settings/base.py b/course_discovery/settings/base.py index b57edab5f5..f3b1ebc77c 100644 --- a/course_discovery/settings/base.py +++ b/course_discovery/settings/base.py @@ -81,6 +81,7 @@ 'course_discovery.apps.publisher', 'course_discovery.apps.publisher_comments', 'course_discovery.apps.journal', + 'course_discovery.apps.edly_discovery_app', ] @@ -637,3 +638,6 @@ } WORDPRESS_APP_AUTH_USERNAME = 'coursediscoveryworker' WORDPRESS_APP_AUTH_PASSWORD = '' + +# Edly configuration +EDLY_PANEL_WORKER_USER = 'edly_panel_worker' diff --git a/course_discovery/urls.py b/course_discovery/urls.py index 2e9be11b8e..bb1d9a10ba 100644 --- a/course_discovery/urls.py +++ b/course_discovery/urls.py @@ -47,6 +47,7 @@ url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^jsi18n/$', JavaScriptCatalog, name='javascript-catalog'), url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), + url(r'^edly_api/', include('course_discovery.apps.edly_discovery_app.urls', namespace='edly_api')), ] # edx-drf-extensions csrf app