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 responses
from django.urls import reverse
......@@ -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.tests.factories import OrganizationFactory
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
class CourseRunViewSet(APITestCase):
def setUp(self):
super().setUp()
self.client.force_login(StaffUserFactory())
class CourseRunViewSetTests(APITestCase):
def test_without_authentication(self):
self.client.logout()
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': 1})
......@@ -29,23 +28,23 @@ class CourseRunViewSet(APITestCase):
response = self.client.post(url, {})
assert response.status_code == 403
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish(self, mock_access_token): # pylint: disable=unused-argument
def _create_course_run_for_publication(self):
organization = OrganizationFactory()
transcript_languages = [LanguageTag.objects.first()]
publisher_course_run = CourseRunFactory(
return CourseRunFactory(
course__organizations=[organization],
course__tertiary_subject=None,
lms_course_id='a/b/c',
transcript_languages=transcript_languages
)
partner = organization.partner
def _set_test_client_domain_and_login(self, partner):
# pylint:disable=attribute-defined-outside-init
self.client = self.client_class(SERVER_NAME=partner.site.domain)
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}
url = '{root}/api/v1/course_runs/{key}/'.format(
root=partner.studio_url.strip('/'),
......@@ -57,13 +56,33 @@ class CourseRunViewSet(APITestCase):
key=publisher_course_run.lms_course_id
)
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)
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})
response = self.client.post(url, {})
assert response.status_code == 200
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)
assert discovery_course_run.title_override == publisher_course_run.title_override
......@@ -77,7 +96,8 @@ class CourseRunViewSet(APITestCase):
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.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
discovery_course = discovery_course_run.course
......@@ -88,11 +108,61 @@ class CourseRunViewSet(APITestCase):
assert discovery_course.full_description == publisher_course.full_description
assert discovery_course.level_type == publisher_course.level_type
assert discovery_course.video == Video.objects.get(src=publisher_course.video_link)
assert list(discovery_course.authoring_organizations.all()) == [organization]
assert set(discovery_course.subjects.all()) == {publisher_course.primary_subject,
publisher_course.secondary_subject}
expected = list(publisher_course_run.course.organizations.all())
assert list(discovery_course.authoring_organizations.all()) == expected
expected = {publisher_course.primary_subject, publisher_course.secondary_subject}
assert set(discovery_course.subjects.all()) == expected
def test_publish_missing_course_run(self):
self.client.force_login(StaffUserFactory())
url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': 1})
response = self.client.post(url, {})
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__)
class CourseRunViewSet(viewsets.GenericViewSet):
authentication_classes = (JwtAuthentication, SessionAuthentication,)
lookup_url_kwarg = 'pk'
queryset = CourseRun.objects.all()
# NOTE: We intentionally use a basic serializer here since there is nothing, yet, to return.
serializer_class = serializers.Serializer
permission_classes = (permissions.IsAdminUser,)
PUBLICATION_SUCCESS_STATUS = 'SUCCESS'
@detail_route(methods=['post'])
def publish(self, request, pk=None):
course_run = self.get_object()
partner = request.site.partner
publication_status = {}
try:
self.publish_to_studio(partner, course_run)
self.publish_to_ecommerce(partner, course_run)
self.publish_to_discovery(partner, course_run)
publication_status['studio'] = self.publish_to_studio(partner, course_run)
publication_status['ecommerce'] = self.publish_to_ecommerce(partner, course_run)
publication_status['discovery'] = self.publish_to_discovery(partner, course_run)
except SlumberBaseException as ex:
logger.exception('Failed to publish course run [%s]!', pk)
content = getattr(ex, 'content', None)
......@@ -41,12 +44,28 @@ class CourseRunViewSet(viewsets.GenericViewSet):
logger.error(content)
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):
api = StudioAPI(partner.studio_api_client)
api.update_course_run_details_in_studio(course_run)
api.update_course_run_image_in_studio(course_run)
try:
api.update_course_run_details_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):
api = EdxRestApiClient(partner.ecommerce_api_url, jwt=partner.access_token)
......@@ -73,7 +92,14 @@ class CourseRunViewSet(viewsets.GenericViewSet):
} for seat in course_run.seats.all()
]
}
api.publication.post(data)
try:
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):
publisher_course = course_run.course
......@@ -124,3 +150,5 @@ class CourseRunViewSet(viewsets.GenericViewSet):
if created:
discovery_course.canonical_course_run = discovery_course_run
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