Commit dab40225 by Clinton Blackburn Committed by Clinton Blackburn

Updated Publisher publish endpoint to return detailed status

The endpoint now returns the publication status of each service to which it published. This will better enable the debugging of errors that occur when publication to one, or more, services fails.

LEARNER-2472
parent 5f7ccf82
import json
import mock import mock
import responses import responses
from django.urls import reverse from django.urls import reverse
...@@ -8,14 +10,11 @@ from course_discovery.apps.core.tests.factories import StaffUserFactory, UserFac ...@@ -8,14 +10,11 @@ from course_discovery.apps.core.tests.factories import StaffUserFactory, UserFac
from course_discovery.apps.course_metadata.models import CourseRun, Video from course_discovery.apps.course_metadata.models import CourseRun, Video
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet
from course_discovery.apps.publisher.tests.factories import CourseRunFactory from course_discovery.apps.publisher.tests.factories import CourseRunFactory
class CourseRunViewSet(APITestCase): class CourseRunViewSetTests(APITestCase):
def setUp(self):
super().setUp()
self.client.force_login(StaffUserFactory())
def test_without_authentication(self): def test_without_authentication(self):
self.client.logout() self.client.logout()
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': 1}) url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': 1})
...@@ -29,23 +28,23 @@ class CourseRunViewSet(APITestCase): ...@@ -29,23 +28,23 @@ class CourseRunViewSet(APITestCase):
response = self.client.post(url, {}) response = self.client.post(url, {})
assert response.status_code == 403 assert response.status_code == 403
@responses.activate def _create_course_run_for_publication(self):
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish(self, mock_access_token): # pylint: disable=unused-argument
organization = OrganizationFactory() organization = OrganizationFactory()
transcript_languages = [LanguageTag.objects.first()] transcript_languages = [LanguageTag.objects.first()]
publisher_course_run = CourseRunFactory( return CourseRunFactory(
course__organizations=[organization], course__organizations=[organization],
course__tertiary_subject=None, course__tertiary_subject=None,
lms_course_id='a/b/c', lms_course_id='a/b/c',
transcript_languages=transcript_languages transcript_languages=transcript_languages
) )
partner = organization.partner
def _set_test_client_domain_and_login(self, partner):
# pylint:disable=attribute-defined-outside-init # pylint:disable=attribute-defined-outside-init
self.client = self.client_class(SERVER_NAME=partner.site.domain) self.client = self.client_class(SERVER_NAME=partner.site.domain)
self.client.force_login(StaffUserFactory()) self.client.force_login(StaffUserFactory())
def _mock_studio_api_success(self, publisher_course_run):
partner = publisher_course_run.course.organizations.first().partner
body = {'id': publisher_course_run.lms_course_id} body = {'id': publisher_course_run.lms_course_id}
url = '{root}/api/v1/course_runs/{key}/'.format( url = '{root}/api/v1/course_runs/{key}/'.format(
root=partner.studio_url.strip('/'), root=partner.studio_url.strip('/'),
...@@ -57,13 +56,33 @@ class CourseRunViewSet(APITestCase): ...@@ -57,13 +56,33 @@ class CourseRunViewSet(APITestCase):
key=publisher_course_run.lms_course_id key=publisher_course_run.lms_course_id
) )
responses.add(responses.POST, url, json=body, status=200) responses.add(responses.POST, url, json=body, status=200)
def _mock_ecommerce_api(self, publisher_course_run, status=200, body=None):
partner = publisher_course_run.course.organizations.first().partner
body = body or {'id': publisher_course_run.lms_course_id}
url = '{root}publication/'.format(root=partner.ecommerce_api_url) url = '{root}publication/'.format(root=partner.ecommerce_api_url)
responses.add(responses.POST, url, json=body, status=200) responses.add(responses.POST, url, json=body, status=status)
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish(self, mock_access_token): # pylint: disable=unused-argument
publisher_course_run = self._create_course_run_for_publication()
partner = publisher_course_run.course.organizations.first().partner
self._set_test_client_domain_and_login(partner)
self._mock_studio_api_success(publisher_course_run)
self._mock_ecommerce_api(publisher_course_run)
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk}) url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk})
response = self.client.post(url, {}) response = self.client.post(url, {})
assert response.status_code == 200 assert response.status_code == 200
assert len(responses.calls) == 3 assert len(responses.calls) == 3
expected = {
'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'ecommerce': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'studio': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
}
assert response.data == expected
discovery_course_run = CourseRun.objects.get(key=publisher_course_run.lms_course_id) discovery_course_run = CourseRun.objects.get(key=publisher_course_run.lms_course_id)
assert discovery_course_run.title_override == publisher_course_run.title_override assert discovery_course_run.title_override == publisher_course_run.title_override
...@@ -77,7 +96,8 @@ class CourseRunViewSet(APITestCase): ...@@ -77,7 +96,8 @@ class CourseRunViewSet(APITestCase):
assert discovery_course_run.min_effort == publisher_course_run.min_effort assert discovery_course_run.min_effort == publisher_course_run.min_effort
assert discovery_course_run.max_effort == publisher_course_run.max_effort assert discovery_course_run.max_effort == publisher_course_run.max_effort
assert discovery_course_run.language == publisher_course_run.language assert discovery_course_run.language == publisher_course_run.language
assert set(discovery_course_run.transcript_languages.all()) == set(transcript_languages) expected = set(publisher_course_run.transcript_languages.all()) # pylint: disable=no-member
assert set(discovery_course_run.transcript_languages.all()) == expected
publisher_course = publisher_course_run.course publisher_course = publisher_course_run.course
discovery_course = discovery_course_run.course discovery_course = discovery_course_run.course
...@@ -88,11 +108,61 @@ class CourseRunViewSet(APITestCase): ...@@ -88,11 +108,61 @@ class CourseRunViewSet(APITestCase):
assert discovery_course.full_description == publisher_course.full_description assert discovery_course.full_description == publisher_course.full_description
assert discovery_course.level_type == publisher_course.level_type assert discovery_course.level_type == publisher_course.level_type
assert discovery_course.video == Video.objects.get(src=publisher_course.video_link) assert discovery_course.video == Video.objects.get(src=publisher_course.video_link)
assert list(discovery_course.authoring_organizations.all()) == [organization] expected = list(publisher_course_run.course.organizations.all())
assert set(discovery_course.subjects.all()) == {publisher_course.primary_subject, assert list(discovery_course.authoring_organizations.all()) == expected
publisher_course.secondary_subject} expected = {publisher_course.primary_subject, publisher_course.secondary_subject}
assert set(discovery_course.subjects.all()) == expected
def test_publish_missing_course_run(self): def test_publish_missing_course_run(self):
self.client.force_login(StaffUserFactory())
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': 1}) url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': 1})
response = self.client.post(url, {}) response = self.client.post(url, {})
assert response.status_code == 404 assert response.status_code == 404
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish_with_studio_api_error(self, mock_access_token): # pylint: disable=unused-argument
publisher_course_run = self._create_course_run_for_publication()
partner = publisher_course_run.course.organizations.first().partner
self._set_test_client_domain_and_login(partner)
expected_error = {'error': 'Oops!'}
url = '{root}/api/v1/course_runs/{key}/'.format(
root=partner.studio_url.strip('/'),
key=publisher_course_run.lms_course_id
)
responses.add(responses.PATCH, url, json=expected_error, status=500)
self._mock_ecommerce_api(publisher_course_run)
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk})
response = self.client.post(url, {})
assert response.status_code == 502
assert len(responses.calls) == 2
expected = {
'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'ecommerce': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'studio': 'FAILED: ' + json.dumps(expected_error),
}
assert response.data == expected
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish_with_ecommerce_api_error(self, mock_access_token): # pylint: disable=unused-argument
publisher_course_run = self._create_course_run_for_publication()
partner = publisher_course_run.course.organizations.first().partner
self._set_test_client_domain_and_login(partner)
expected_error = {'error': 'Oops!'}
self._mock_studio_api_success(publisher_course_run)
self._mock_ecommerce_api(publisher_course_run, status=500, body=expected_error)
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk})
response = self.client.post(url, {})
assert response.status_code == 502
assert len(responses.calls) == 3
expected = {
'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
'ecommerce': 'FAILED: ' + json.dumps(expected_error),
'studio': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS,
}
assert response.data == expected
...@@ -19,21 +19,24 @@ logger = logging.getLogger(__name__) ...@@ -19,21 +19,24 @@ logger = logging.getLogger(__name__)
class CourseRunViewSet(viewsets.GenericViewSet): class CourseRunViewSet(viewsets.GenericViewSet):
authentication_classes = (JwtAuthentication, SessionAuthentication,) authentication_classes = (JwtAuthentication, SessionAuthentication,)
lookup_url_kwarg = 'pk'
queryset = CourseRun.objects.all() queryset = CourseRun.objects.all()
# NOTE: We intentionally use a basic serializer here since there is nothing, yet, to return. # NOTE: We intentionally use a basic serializer here since there is nothing, yet, to return.
serializer_class = serializers.Serializer serializer_class = serializers.Serializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
PUBLICATION_SUCCESS_STATUS = 'SUCCESS'
@detail_route(methods=['post']) @detail_route(methods=['post'])
def publish(self, request, pk=None): def publish(self, request, pk=None):
course_run = self.get_object() course_run = self.get_object()
partner = request.site.partner partner = request.site.partner
publication_status = {}
try: try:
self.publish_to_studio(partner, course_run) publication_status['studio'] = self.publish_to_studio(partner, course_run)
self.publish_to_ecommerce(partner, course_run) publication_status['ecommerce'] = self.publish_to_ecommerce(partner, course_run)
self.publish_to_discovery(partner, course_run) publication_status['discovery'] = self.publish_to_discovery(partner, course_run)
except SlumberBaseException as ex: except SlumberBaseException as ex:
logger.exception('Failed to publish course run [%s]!', pk) logger.exception('Failed to publish course run [%s]!', pk)
content = getattr(ex, 'content', None) content = getattr(ex, 'content', None)
...@@ -41,12 +44,28 @@ class CourseRunViewSet(viewsets.GenericViewSet): ...@@ -41,12 +44,28 @@ class CourseRunViewSet(viewsets.GenericViewSet):
logger.error(content) logger.error(content)
raise raise
return Response({}, status=status.HTTP_200_OK) status_code = status.HTTP_200_OK
for _status in publication_status.values():
if not _status.startswith(self.PUBLICATION_SUCCESS_STATUS):
status_code = status.HTTP_502_BAD_GATEWAY
break
return Response(publication_status, status=status_code)
def publish_to_studio(self, partner, course_run): def publish_to_studio(self, partner, course_run):
api = StudioAPI(partner.studio_api_client) api = StudioAPI(partner.studio_api_client)
try:
api.update_course_run_details_in_studio(course_run) api.update_course_run_details_in_studio(course_run)
api.update_course_run_image_in_studio(course_run) api.update_course_run_image_in_studio(course_run)
return self.PUBLICATION_SUCCESS_STATUS
except SlumberBaseException as ex:
content = ex.content.decode('utf8')
logger.exception('Failed to publish course run [%d] to Studio! Error was: [%s]', course_run.pk, content)
return 'FAILED: ' + content
except Exception as ex: # pylint: disable=broad-except
logger.exception('Failed to publish course run [%d] to Studio!', course_run.pk)
return 'FAILED: ' + str(ex)
def publish_to_ecommerce(self, partner, course_run): def publish_to_ecommerce(self, partner, course_run):
api = EdxRestApiClient(partner.ecommerce_api_url, jwt=partner.access_token) api = EdxRestApiClient(partner.ecommerce_api_url, jwt=partner.access_token)
...@@ -73,7 +92,14 @@ class CourseRunViewSet(viewsets.GenericViewSet): ...@@ -73,7 +92,14 @@ class CourseRunViewSet(viewsets.GenericViewSet):
} for seat in course_run.seats.all() } for seat in course_run.seats.all()
] ]
} }
try:
api.publication.post(data) api.publication.post(data)
return self.PUBLICATION_SUCCESS_STATUS
except SlumberBaseException as ex:
content = ex.content.decode('utf8')
logger.exception('Failed to publish course run [%d] to E-Commerce! Error was: [%s]', course_run.pk, content)
return 'FAILED: ' + content
def publish_to_discovery(self, partner, course_run): def publish_to_discovery(self, partner, course_run):
publisher_course = course_run.course publisher_course = course_run.course
...@@ -124,3 +150,5 @@ class CourseRunViewSet(viewsets.GenericViewSet): ...@@ -124,3 +150,5 @@ class CourseRunViewSet(viewsets.GenericViewSet):
if created: if created:
discovery_course.canonical_course_run = discovery_course_run discovery_course.canonical_course_run = discovery_course_run
discovery_course.save() discovery_course.save()
return self.PUBLICATION_SUCCESS_STATUS
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment