Commit 9d270564 by Clinton Blackburn Committed by Clinton Blackburn

Publishing course runs to Studio upon creation

LEARNER-2469
parent 3585eda5
......@@ -48,7 +48,9 @@ def make_request():
return request
def serialize_datetime(d):
def serialize_datetime_without_timezone(d):
# TODO: Remove this function, and replace usage of it with serialize_datetime, after
# https://github.com/encode/django-rest-framework/issues/3732 is released.
return d.strftime('%Y-%m-%dT%H:%M:%S') if d else None
......@@ -1200,10 +1202,10 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'max_effort': course_run.max_effort,
'weeks_to_complete': course_run.weeks_to_complete,
'short_description': course_run.short_description,
'start': serialize_datetime(course_run.start),
'end': serialize_datetime(course_run.end),
'enrollment_start': serialize_datetime(course_run.enrollment_start),
'enrollment_end': serialize_datetime(course_run.enrollment_end),
'start': serialize_datetime_without_timezone(course_run.start),
'end': serialize_datetime_without_timezone(course_run.end),
'enrollment_start': serialize_datetime_without_timezone(course_run.enrollment_start),
'enrollment_end': serialize_datetime_without_timezone(course_run.enrollment_end),
'key': course_run.key,
'marketing_url': course_run.marketing_url,
'pacing_type': course_run.pacing_type,
......
""" Core models. """
import datetime
from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from edx_rest_api_client.client import EdxRestApiClient
from guardian.mixins import GuardianUserMixin
......@@ -91,3 +95,32 @@ class Partner(TimeStampedModel):
@property
def has_marketing_site(self):
return bool(self.marketing_site_url_root)
@property
def access_token(self):
""" Returns an access token for this site's service user.
Returns:
str: JWT access token
"""
key = 'partner_access_token_{}'.format(self.id)
access_token = cache.get(key)
if not access_token:
url = '{root}/access_token'.format(root=self.oidc_url_root)
access_token, expiration_datetime = EdxRestApiClient.get_oauth_access_token(
url,
self.oidc_key,
self.oidc_secret,
token_type='jwt'
)
expires = (expiration_datetime - datetime.datetime.utcnow()).seconds
cache.set(key, access_token, expires)
return access_token
@cached_property
def studio_api_client(self):
studio_api_url = '{root}/api/v1/'.format(root=self.studio_url.strip('/'))
return EdxRestApiClient(studio_api_url, jwt=self.access_token)
......@@ -40,18 +40,18 @@ class PartnerFactory(factory.DjangoModelFactory):
name = factory.Sequence(lambda n: 'test-partner-{}'.format(n)) # pylint: disable=unnecessary-lambda
short_code = factory.Sequence(lambda n: 'test{}'.format(n)) # pylint: disable=unnecessary-lambda
courses_api_url = '{root}/api/courses/v1/'.format(root=FuzzyUrlRoot().fuzz())
ecommerce_api_url = '{root}/api/courses/v1/'.format(root=FuzzyUrlRoot().fuzz())
ecommerce_api_url = '{root}/api/v2/'.format(root=FuzzyUrlRoot().fuzz())
organizations_api_url = '{root}/api/organizations/v1/'.format(root=FuzzyUrlRoot().fuzz())
programs_api_url = '{root}/api/programs/v1/'.format(root=FuzzyUrlRoot().fuzz())
marketing_site_api_url = '{root}/api/courses/v1/'.format(root=FuzzyUrlRoot().fuzz())
marketing_site_url_root = '{root}/'.format(root=FuzzyUrlRoot().fuzz())
marketing_site_url_root = factory.Faker('url')
marketing_site_api_username = factory.Faker('user_name')
marketing_site_api_password = factory.Faker('password')
oidc_url_root = '{root}'.format(root=FuzzyUrlRoot().fuzz())
oidc_url_root = factory.Faker('url')
oidc_key = factory.Faker('sha256')
oidc_secret = factory.Faker('sha256')
site = factory.SubFactory(SiteFactory)
studio_url = FuzzyUrlRoot().fuzz()
studio_url = factory.Faker('url')
class Meta(object):
model = Partner
""" Tests for core models. """
import ddt
import responses
from django.test import TestCase
from social_django.models import UserSocialAuth
......@@ -75,3 +76,21 @@ class PartnerTests(TestCase):
def test_has_marketing_site(self, marketing_site_url_root, expected):
partner = PartnerFactory(marketing_site_url_root=marketing_site_url_root)
self.assertEqual(partner.has_marketing_site, expected) # pylint: disable=no-member
@responses.activate
def test_access_token(self):
""" Verify the property retrieves, and caches, an access token from the OAuth 2.0 provider. """
token = 'abc123'
partner = PartnerFactory()
url = '{root}/access_token'.format(root=partner.oidc_url_root)
body = {
'access_token': token,
'expires_in': 3600,
}
responses.add(responses.POST, url, json=body, status=200)
assert partner.access_token == token
assert len(responses.calls) == 1
# No HTTP calls should be made if the access token is cached.
responses.reset()
assert partner.access_token == token
......@@ -8,6 +8,10 @@ from course_discovery.settings.process_synonyms import get_synonyms
logger = logging.getLogger(__name__)
def serialize_datetime(d):
return d.strftime('%Y-%m-%dT%H:%M:%SZ') if d else None
class ElasticsearchUtils(object):
@classmethod
def create_alias_and_index(cls, es_connection, alias):
......@@ -84,6 +88,7 @@ class SearchQuerySetWrapper(object):
"""
Decorates a SearchQuerySet object using a generator for efficient iteration
"""
def __init__(self, qs):
self.qs = qs
......
......@@ -146,7 +146,7 @@ class SeatFactory(factory.DjangoModelFactory):
class OrganizationFactory(factory.DjangoModelFactory):
uuid = factory.LazyFunction(uuid4)
key = FuzzyText(prefix='Org.fake/')
key = FuzzyText()
name = FuzzyText()
description = FuzzyText()
homepage_url = FuzzyURL()
......
default_app_config = 'course_discovery.apps.publisher.apps.PublisherAppConfig'
from django.apps import AppConfig
class PublisherAppConfig(AppConfig):
name = 'course_discovery.apps.publisher'
verbose_name = 'Publisher'
def ready(self):
super().ready()
# noinspection PyUnresolvedReferences
import course_discovery.apps.publisher.signals # pylint: disable=unused-variable
......@@ -6,6 +6,7 @@ from django.contrib.auth.models import Group
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from django_fsm import FSMField, transition
......@@ -16,6 +17,7 @@ from taggit.managers import TaggableManager
from course_discovery.apps.core.models import Currency, User
from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.models import Course as DiscoveryCourse
from course_discovery.apps.course_metadata.models import LevelType, Organization, Person, Subject
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -253,6 +255,11 @@ class Course(TimeStampedModel, ChangedByMixin):
return self.title
@cached_property
def discovery_counterpart(self):
course_key = '{org}+{number}'.format(org=self.organizations.first().key, number=self.number)
return DiscoveryCourse.objects.get(partner=self.partner, key=course_key)
class CourseRun(TimeStampedModel, ChangedByMixin):
""" Publisher CourseRun model. It contains fields related to the course run intake form."""
......
import logging
import waffle
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.signals import post_save
from django.dispatch import receiver
from slumber.exceptions import SlumberBaseException
from course_discovery.apps.publisher.models import CourseRun
from course_discovery.apps.publisher.studio_api_utils import StudioAPI
logger = logging.getLogger(__name__)
def get_related_discovery_course_run(publisher_course_run):
discovery_course = publisher_course_run.course.discovery_counterpart
return discovery_course.course_runs.latest('start')
@receiver(post_save, sender=CourseRun)
def create_course_run_in_studio_receiver(sender, instance, created, **kwargs): # pylint: disable=unused-argument
if created and waffle.switch_is_active('enable_publisher_create_course_run_in_studio'):
course = instance.course
partner = course.partner
if not partner:
logger.error('Failed to publish course run [%d] to Studio. Related course [%d] has no associated Partner.',
instance.id, course.id)
return
logger.info('Publishing course run [%d] to Studio...', instance.id)
api = StudioAPI(instance.course.partner.studio_api_client)
# TODO How to handle the fact that Publisher does not expose enrollment date fields?
instance.enrollment_start = instance.enrollment_start or instance.start
instance.enrollment_end = instance.enrollment_end or instance.end
try:
try:
discovery_course_run = get_related_discovery_course_run(instance)
response = api.create_course_rerun_in_studio(instance, discovery_course_run)
except ObjectDoesNotExist:
response = api.create_course_run_in_studio(instance)
instance.lms_course_id = response['id']
instance.save()
except SlumberBaseException as ex:
logger.exception('Failed to create course run [%d] on Studio: %s', instance.id, ex.content)
raise
try:
api.update_course_run_image_in_studio(instance)
except SlumberBaseException as ex:
logger.exception(
'Failed to update Studio image for course run [%s]: %s', instance.lms_course_id, ex.content
)
logger.info('Completed creation of course run [%s] on Studio.', instance.lms_course_id)
import logging
import math
from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.core.utils import serialize_datetime
logger = logging.getLogger(__name__)
class StudioAPI:
def __init__(self, api_client):
self._api = api_client
@classmethod
def _get_next_run(cls, root, suffix, existing_runs):
candidate = root + suffix
if candidate in existing_runs:
# If our candidate is an existing run, use the next letter in the alphabet as the
# run suffix (e.g. 1T2017, 1T2017a, 1T2017b, ...).
suffix = chr(ord(suffix) + 1) if suffix else 'a'
return cls._get_next_run(root, suffix, existing_runs)
return candidate
@classmethod
def calculate_course_run_key_run_value(cls, course_run):
start = course_run.start
trimester = math.ceil(start.month / 4.)
run = '{trimester}T{year}'.format(trimester=trimester, year=start.year)
try:
discovery_course = course_run.course.discovery_counterpart
except: # pylint:disable=bare-except
logger.exception('Failed to get Discovery counterpart for Publisher course [%d]', course_run.course.id)
return run
related_course_runs = discovery_course.course_runs.values_list('key', flat=True)
related_course_runs = [CourseKey.from_string(key).run for key in related_course_runs]
return cls._get_next_run(run, '', related_course_runs)
@classmethod
def generate_data_for_studio_api(cls, publisher_course_run):
course = publisher_course_run.course
course_team_admin = course.course_team_admin
team = []
if course_team_admin:
team = [
{
'user': course_team_admin.username,
'role': 'instructor',
},
]
return {
'title': publisher_course_run.title_override or course.title,
'org': course.organizations.first().key,
'number': course.number,
'run': cls.calculate_course_run_key_run_value(publisher_course_run),
'schedule': {
'start': serialize_datetime(publisher_course_run.start),
'end': serialize_datetime(publisher_course_run.end),
'enrollment_start': serialize_datetime(publisher_course_run.enrollment_start),
'enrollment_end': serialize_datetime(publisher_course_run.enrollment_end),
},
'team': team,
}
def create_course_rerun_in_studio(self, publisher_course_run, discovery_course_run):
data = self.generate_data_for_studio_api(publisher_course_run)
return self._api.course_runs(discovery_course_run.key).rerun.post(data)
def create_course_run_in_studio(self, publisher_course_run):
data = self.generate_data_for_studio_api(publisher_course_run)
return self._api.course_runs.post(data)
def update_course_run_image_in_studio(self, publisher_course_run):
files = {'card_image': publisher_course_run.course.image}
return self._api.course_runs(publisher_course_run.lms_course_id).images.post(files=files)
......@@ -28,6 +28,7 @@ class CourseFactory(factory.DjangoModelFactory):
syllabus = FuzzyText()
learner_testimonial = FuzzyText()
level_type = factory.SubFactory(factories.LevelTypeFactory)
image = factory.django.ImageField()
primary_subject = factory.SubFactory(factories.SubjectFactory)
secondary_subject = factory.SubFactory(factories.SubjectFactory)
......
......@@ -246,7 +246,9 @@ class PublisherCustomCourseFormTests(TestCase):
Verify that course_title is properly escaped and saved in database while
updating the course
"""
course, course_admin = self.setup_course(title='test_course')
course, course_admin = self.setup_course(image=None)
assert course.title != 'áçã'
organization = course.organizations.first().id
course_from_data = {
'title': 'áçã',
......@@ -258,6 +260,7 @@ class PublisherCustomCourseFormTests(TestCase):
**{'data': course_from_data, 'instance': course, 'user': course_admin,
'organization': organization}
)
self.assertTrue(course_form.is_valid())
course_updated_data = course_form.save()
self.assertTrue(course_updated_data.title, 'áçã')
assert course_form.is_valid()
course_form.save()
course.refresh_from_db()
assert course.title == 'áçã'
......@@ -43,27 +43,24 @@ class CourseRunTests(TestCase):
self.assertIsNone(self.course_run.created_by)
user = UserFactory()
history_object = self.course_run.history.first()
history_object = self.course_run.history.order_by('history_date').first()
history_object.history_user = user
history_object.save()
self.assertEqual(self.course_run.created_by, user.get_full_name())
assert self.course_run.created_by == user.get_full_name()
def test_studio_url(self):
""" Verify that property returns studio url. """
self.assertFalse(self.course_run.studio_url)
assert self.course_run.studio_url is None
# save the lms course id and save the organization.
self.course_run.lms_course_id = 'test'
self.course_run.save()
organization = OrganizationFactory()
self.course_run.course.organizations.add(organization)
self.assertEqual(self.course_run.course.partner, organization.partner)
assert self.course_run.course.partner == organization.partner
self.assertEqual(
'{url}/course/{id}'.format(url=self.course_run.course.partner.studio_url, id='test'),
self.course_run.studio_url
)
actual = '{url}/course/{id}'.format(url=self.course_run.course.partner.studio_url.strip('/'),
id=self.course_run.lms_course_id)
assert actual == self.course_run.studio_url
def test_has_valid_staff(self):
""" Verify that property returns True if course-run must have a staff member
......@@ -331,24 +328,19 @@ class CourseTests(TestCase):
self.assertEqual(self.user1, self.course2.publisher)
def test_course_image_url(self):
""" Verify that the property returns the course image url. """
self.assertIsNone(self.course.course_image_url)
course = factories.CourseFactory(image=None)
assert course.course_image_url is None
# Create a published course-run with card_image_url.
course_run = factories.CourseRunFactory(course=self.course)
course_run = factories.CourseRunFactory(course=course)
factories.CourseRunStateFactory(course_run=course_run, name=CourseRunStateChoices.Published)
course_run.card_image_url = 'http://example.com/test.jpg'
course_run.save()
# Verify that property returns card_image_url of course-run.
self.assertEqual(self.course.course_image_url, course_run.card_image_url)
assert course.course_image_url == course_run.card_image_url
# Create a course image.
self.course.image = make_image_file('test_banner1.jpg')
self.course.save()
# Verify that property returns course image field url.
self.assertEqual(self.course.course_image_url, self.course.image.url)
course.image = make_image_file('test_banner1.jpg')
course.save()
assert course.course_image_url == course.image.url
def test_short_description_override(self):
""" Verify that the property returns the short_description. """
......
import datetime
import json
import mock
import pytest
import responses
from slumber.exceptions import HttpServerError
from waffle.testutils import override_switch
from course_discovery.apps.core.models import Partner
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory as DiscoveryCourseRunFactory
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.studio_api_utils import StudioAPI
from course_discovery.apps.publisher.tests.factories import CourseRunFactory
@pytest.mark.django_db
class TestSignals:
@override_switch('enable_publisher_create_course_run_in_studio', active=True)
def test_create_course_run_in_studio_without_partner(self):
with mock.patch('course_discovery.apps.publisher.signals.logger.error') as mock_logger:
publisher_course_run = CourseRunFactory(course__organizations=[])
assert publisher_course_run.course.partner is None
mock_logger.assert_called_with(
'Failed to publish course run [%d] to Studio. Related course [%d] has no associated Partner.',
publisher_course_run.id,
publisher_course_run.course.id
)
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
@override_switch('enable_publisher_create_course_run_in_studio', active=True)
def test_create_course_run_in_studio(self, mock_access_token): # pylint: disable=unused-argument
organization = OrganizationFactory()
partner = organization.partner
start = datetime.datetime.utcnow()
run = StudioAPI.calculate_course_run_key_run_value(CourseRunFactory.build(start=start))
course_run_key = 'course-v1:TestX+Testing101x+' + run
body = {'id': course_run_key}
studio_url_root = partner.studio_url.strip('/')
url = '{}/api/v1/course_runs/'.format(studio_url_root)
responses.add(responses.POST, url, json=body, status=200)
body = {'card_image': 'https://example.com/image.jpg'}
url = '{root}/api/v1/course_runs/{course_run_key}/images/'.format(
root=studio_url_root,
course_run_key=course_run_key
)
responses.add(responses.POST, url, json=body, status=200)
publisher_course_run = CourseRunFactory(start=start, lms_course_id=None, course__organizations=[organization])
# We refresh because the signal should update the instance with the course run key from Studio
publisher_course_run.refresh_from_db()
assert len(responses.calls) == 2
assert publisher_course_run.lms_course_id == course_run_key
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
@override_switch('enable_publisher_create_course_run_in_studio', active=True)
def test_create_course_run_in_studio_as_rerun(self, mock_access_token): # pylint: disable=unused-argument
number = 'TestX'
organization = OrganizationFactory()
partner = organization.partner
course_key = '{org}+{number}'.format(org=organization.key, number=number)
discovery_course_run = DiscoveryCourseRunFactory(course__partner=partner, course__key=course_key)
start = datetime.datetime.utcnow()
run = StudioAPI.calculate_course_run_key_run_value(CourseRunFactory.build(start=start))
course_run_key = 'course-v1:TestX+Testing101x+' + run
body = {'id': course_run_key}
studio_url_root = partner.studio_url.strip('/')
url = '{root}/api/v1/course_runs/{course_run_key}/rerun/'.format(
root=studio_url_root,
course_run_key=discovery_course_run.key
)
responses.add(responses.POST, url, json=body, status=200)
body = {'card_image': 'https://example.com/image.jpg'}
url = '{root}/api/v1/course_runs/{course_run_key}/images/'.format(
root=studio_url_root,
course_run_key=course_run_key
)
responses.add(responses.POST, url, json=body, status=200)
publisher_course_run = CourseRunFactory(
start=start,
lms_course_id=None,
course__organizations=[organization],
course__number=number
)
# We refresh because the signal should update the instance with the course run key from Studio
publisher_course_run.refresh_from_db()
assert len(responses.calls) == 2
assert publisher_course_run.lms_course_id == course_run_key
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
@override_switch('enable_publisher_create_course_run_in_studio', active=True)
def test_create_course_run_in_studio_with_image_failure(self, mock_access_token): # pylint: disable=unused-argument
organization = OrganizationFactory()
partner = organization.partner
start = datetime.datetime.utcnow()
run = StudioAPI.calculate_course_run_key_run_value(CourseRunFactory.build(start=start))
course_run_key = 'course-v1:TestX+Testing101x+' + run
body = {'id': course_run_key}
studio_url_root = partner.studio_url.strip('/')
url = '{}/api/v1/course_runs/'.format(studio_url_root)
responses.add(responses.POST, url, json=body, status=200)
body = {'error': 'Server error'}
url = '{root}/api/v1/course_runs/{course_run_key}/images/'.format(
root=studio_url_root,
course_run_key=course_run_key
)
responses.add(responses.POST, url, json=body, status=500)
with mock.patch('course_discovery.apps.publisher.signals.logger.exception') as mock_logger:
publisher_course_run = CourseRunFactory(
start=start,
lms_course_id=None,
course__organizations=[organization]
)
assert len(responses.calls) == 2
assert publisher_course_run.lms_course_id == course_run_key
mock_logger.assert_called_with(
'Failed to update Studio image for course run [%s]: %s', course_run_key, json.dumps(body).encode('utf8')
)
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
@override_switch('enable_publisher_create_course_run_in_studio', active=True)
def test_create_course_run_in_studio_with_api_failure(self, mock_access_token): # pylint: disable=unused-argument
organization = OrganizationFactory()
partner = organization.partner
body = {'error': 'Server error'}
studio_url_root = partner.studio_url.strip('/')
url = '{}/api/v1/course_runs/'.format(studio_url_root)
responses.add(responses.POST, url, json=body, status=500)
with mock.patch('course_discovery.apps.publisher.signals.logger.exception') as mock_logger:
with pytest.raises(HttpServerError):
publisher_course_run = CourseRunFactory(lms_course_id=None, course__organizations=[organization])
assert len(responses.calls) == 1
assert publisher_course_run.lms_course_id is None
mock_logger.assert_called_with(
'Failed to create course run [%d] on Studio: %s',
publisher_course_run.id,
json.dumps(body).encode('utf8')
)
import datetime
from itertools import product
import pytest
from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.course_metadata.tests.factories import CourseFactory as DiscoveryCourseFactory
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory as DiscoveryCourseRunFactory
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.studio_api_utils import StudioAPI
from course_discovery.apps.publisher.tests.factories import CourseRunFactory, CourseUserRoleFactory
test_data = list(product(range(1, 5), ['1T2017'])) + list(product(range(5, 8), ['2T2017'])) + \
list(product(range(9, 13), ['3T2017']))
@pytest.mark.django_db
@pytest.mark.parametrize('month,expected', test_data)
def test_calculate_course_run_key_run_value(month, expected):
course_run = CourseRunFactory(start=datetime.datetime(2017, month, 1))
assert StudioAPI.calculate_course_run_key_run_value(course_run) == expected
@pytest.mark.django_db
def test_calculate_course_run_key_run_value_with_multiple_runs_per_trimester():
number = 'TestX'
organization = OrganizationFactory()
partner = organization.partner
course_key = '{org}+{number}'.format(org=organization.key, number=number)
discovery_course = DiscoveryCourseFactory(partner=partner, key=course_key)
DiscoveryCourseRunFactory(key='course-v1:TestX+Testing101x+1T2017', course=discovery_course)
course_run = CourseRunFactory(
start=datetime.datetime(2017, 2, 1),
lms_course_id=None,
course__organizations=[organization],
course__number=number
)
assert StudioAPI.calculate_course_run_key_run_value(course_run) == '1T2017a'
DiscoveryCourseRunFactory(key='course-v1:TestX+Testing101x+1T2017a', course=discovery_course)
assert StudioAPI.calculate_course_run_key_run_value(course_run) == '1T2017b'
@pytest.mark.django_db
@pytest.mark.parametrize('with_team', (True, False,))
def test_generate_data_for_studio_api(with_team):
course_run = CourseRunFactory(course__organizations=[OrganizationFactory()])
course = course_run.course
team = []
if with_team:
role = CourseUserRoleFactory(course=course, role=PublisherUserRole.CourseTeam)
team = [
{
'user': role.user.username,
'role': 'instructor',
},
]
expected = {
'title': course_run.title_override or course.title,
'org': course.organizations.first().key,
'number': course.number,
'run': StudioAPI.calculate_course_run_key_run_value(course_run),
'schedule': {
'start': serialize_datetime(course_run.start),
'end': serialize_datetime(course_run.end),
'enrollment_start': serialize_datetime(course_run.enrollment_start),
'enrollment_end': serialize_datetime(course_run.enrollment_end),
},
'team': team,
}
assert StudioAPI.generate_data_for_studio_api(course_run) == expected
......@@ -1973,7 +1973,7 @@ class CourseDetailViewTests(TestCase):
def setUp(self):
super(CourseDetailViewTests, self).setUp()
self.organization_extension = factories.OrganizationExtensionFactory()
self.course = factories.CourseFactory(organizations=[self.organization_extension.organization])
self.course = factories.CourseFactory(organizations=[self.organization_extension.organization], image=None)
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
......@@ -2395,7 +2395,7 @@ class CourseEditViewTests(SiteMixin, TestCase):
def setUp(self):
super(CourseEditViewTests, self).setUp()
self.organization_extension = factories.OrganizationExtensionFactory()
self.course = factories.CourseFactory(organizations=[self.organization_extension.organization])
self.course = factories.CourseFactory(organizations=[self.organization_extension.organization], image=None)
self.user = UserFactory()
self.course_team_user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
......
......@@ -162,13 +162,14 @@ class CourseRunWrapperTests(TestCase):
self.assertEqual(self.wrapped_course_run.course_team_admin, self.course.course_team_admin)
def test_course_image_url(self):
""" Verify that the wrapper return the course image url. """
self.assertIsNone(self.wrapped_course_run.course_image_url)
self.course.image = make_image_file('test_banner1.jpg')
self.course.save()
course_run = factories.CourseRunFactory(course__image=None)
wrapped_course_run = CourseRunWrapper(course_run)
assert wrapped_course_run.course_image_url is None
self.assertEqual(self.wrapped_course_run.course_image_url, self.course.image.url)
course = course_run.course
course.image = make_image_file('test_banner1.jpg')
course.save()
assert wrapped_course_run.course_image_url == course.image.url
def test_course_staff(self):
"""Verify that the wrapper return staff list."""
......
......@@ -13,6 +13,6 @@ pep8==1.7.0
pytest==3.0.6
pytest-django==3.1.2
pytest-django-ordering==1.0.1
responses==0.5.1
responses==0.7.0
selenium==3.4.0
testfixtures==4.13.1
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