Merge pull request #14510 from edx/mrehan/mgmt-cmd-sync-mktg-urls
MA-3050 – Sync course runs from Catalog service
This commit is contained in:
@@ -1103,13 +1103,17 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
""" Initialize course used to test enrollment fields. """
|
||||
"""
|
||||
Initialize course used to test enrollment fields.
|
||||
"""
|
||||
super(CourseEnrollmentEndFieldTest, self).setUp()
|
||||
self.course = CourseFactory.create(org='edX', number='dummy', display_name='Marketing Site Course')
|
||||
self.course_details_url = reverse_course_url('settings_handler', unicode(self.course.id))
|
||||
|
||||
def _get_course_details_response(self, global_staff):
|
||||
""" Return the course details page as either global or non-global staff"""
|
||||
"""
|
||||
Return the course details page as either global or non-global staff
|
||||
"""
|
||||
user = UserFactory(is_staff=global_staff)
|
||||
CourseInstructorRole(self.course.id).add_users(user)
|
||||
|
||||
@@ -1118,7 +1122,8 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
return self.client.get_html(self.course_details_url)
|
||||
|
||||
def _verify_editable(self, response):
|
||||
""" Verify that the response has expected editable fields.
|
||||
"""
|
||||
Verify that the response has expected editable fields.
|
||||
|
||||
Assert that all editable field content exists and no
|
||||
uneditable field content exists for enrollment end fields.
|
||||
@@ -1131,7 +1136,8 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
self.assertContains(response, element)
|
||||
|
||||
def _verify_not_editable(self, response):
|
||||
""" Verify that the response has expected non-editable fields.
|
||||
"""
|
||||
Verify that the response has expected non-editable fields.
|
||||
|
||||
Assert that all uneditable field content exists and no
|
||||
editable field content exists for enrollment end fields.
|
||||
@@ -1145,7 +1151,8 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': False})
|
||||
def test_course_details_with_disabled_setting_global_staff(self):
|
||||
""" Test that user enrollment end date is editable in response.
|
||||
"""
|
||||
Test that user enrollment end date is editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is not enabled.
|
||||
User is global staff.
|
||||
@@ -1154,7 +1161,8 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': False})
|
||||
def test_course_details_with_disabled_setting_non_global_staff(self):
|
||||
""" Test that user enrollment end date is editable in response.
|
||||
"""
|
||||
Test that user enrollment end date is editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is not enabled.
|
||||
User is non-global staff.
|
||||
@@ -1164,7 +1172,8 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': True})
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_course_details_with_enabled_setting_global_staff(self):
|
||||
""" Test that user enrollment end date is editable in response.
|
||||
"""
|
||||
Test that user enrollment end date is editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is enabled.
|
||||
User is global staff.
|
||||
@@ -1174,7 +1183,8 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': True})
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_course_details_with_enabled_setting_non_global_staff(self):
|
||||
""" Test that user enrollment end date is not editable in response.
|
||||
"""
|
||||
Test that user enrollment end date is not editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is enabled.
|
||||
User is non-global staff.
|
||||
|
||||
@@ -74,7 +74,7 @@ from student.auth import has_course_author_access, has_studio_write_access, has_
|
||||
from student.roles import (
|
||||
CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff, UserBasedRole
|
||||
)
|
||||
from util.course import get_lms_link_for_about_page
|
||||
from util.course import get_link_for_about_page
|
||||
from util.date_utils import get_default_time_display
|
||||
from util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
|
||||
from util.milestones_helpers import (
|
||||
@@ -987,7 +987,7 @@ def settings_handler(request, course_key_string):
|
||||
settings_context = {
|
||||
'context_course': course_module,
|
||||
'course_locator': course_key,
|
||||
'lms_link_for_about_page': get_lms_link_for_about_page(course_key),
|
||||
'lms_link_for_about_page': get_link_for_about_page(course_module),
|
||||
'course_image_url': course_image_url(course_module, 'course_image'),
|
||||
'banner_image_url': course_image_url(course_module, 'banner_image'),
|
||||
'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'),
|
||||
|
||||
@@ -14,7 +14,8 @@ from openedx.core.djangoapps.credit.signals import on_course_publish
|
||||
|
||||
|
||||
class CreditEligibilityTest(CourseTestCase):
|
||||
"""Base class to test the course settings details view in Studio for credit
|
||||
"""
|
||||
Base class to test the course settings details view in Studio for credit
|
||||
eligibility requirements.
|
||||
"""
|
||||
def setUp(self):
|
||||
@@ -24,7 +25,8 @@ class CreditEligibilityTest(CourseTestCase):
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': False})
|
||||
def test_course_details_with_disabled_setting(self):
|
||||
"""Test that user don't see credit eligibility requirements in response
|
||||
"""
|
||||
Test that user don't see credit eligibility requirements in response
|
||||
if the feature flag 'ENABLE_CREDIT_ELIGIBILITY' is not enabled.
|
||||
"""
|
||||
response = self.client.get_html(self.course_details_url)
|
||||
@@ -34,7 +36,8 @@ class CreditEligibilityTest(CourseTestCase):
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': True})
|
||||
def test_course_details_with_enabled_setting(self):
|
||||
"""Test that credit eligibility requirements are present in
|
||||
"""
|
||||
Test that credit eligibility requirements are present in
|
||||
response if the feature flag 'ENABLE_CREDIT_ELIGIBILITY' is enabled.
|
||||
"""
|
||||
# verify that credit eligibility requirements block don't show if the
|
||||
|
||||
@@ -4,25 +4,26 @@ Utility methods related to course
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_lms_link_for_about_page(course_key):
|
||||
def get_link_for_about_page(course):
|
||||
"""
|
||||
Returns the url to the course about page.
|
||||
"""
|
||||
assert isinstance(course_key, CourseKey)
|
||||
Arguments:
|
||||
course: This can be either a course overview object or a course descriptor.
|
||||
|
||||
if settings.FEATURES.get('ENABLE_MKTG_SITE'):
|
||||
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
|
||||
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
|
||||
about_base = settings.MKTG_URLS['ROOT']
|
||||
Returns the course sharing url, this can be one of course's social sharing url, marketing url, or
|
||||
lms course about url.
|
||||
"""
|
||||
is_social_sharing_enabled = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get('CUSTOM_COURSE_URLS')
|
||||
if is_social_sharing_enabled and course.social_sharing_url:
|
||||
course_about_url = course.social_sharing_url
|
||||
elif settings.FEATURES.get('ENABLE_MKTG_SITE') and getattr(course, 'marketing_url', None):
|
||||
course_about_url = course.marketing_url
|
||||
else:
|
||||
about_base = settings.LMS_ROOT_URL
|
||||
course_about_url = u'{about_base_url}/courses/{course_key}/about'.format(
|
||||
about_base_url=settings.LMS_ROOT_URL,
|
||||
course_key=unicode(course.id),
|
||||
)
|
||||
|
||||
return u"{about_base_url}/courses/{course_key}/about".format(
|
||||
about_base_url=about_base,
|
||||
course_key=course_key.to_deprecated_string()
|
||||
)
|
||||
return course_about_url
|
||||
|
||||
@@ -1,36 +1,129 @@
|
||||
"""
|
||||
Tests for course utils.
|
||||
"""
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
import ddt
|
||||
import mock
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from util.course import get_lms_link_for_about_page
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from util.course import get_link_for_about_page
|
||||
|
||||
|
||||
class LmsLinksTestCase(TestCase):
|
||||
""" Tests for LMS links. """
|
||||
@ddt.ddt
|
||||
class TestCourseSharingLinks(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for course sharing links.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestCourseSharingLinks, self).setUp()
|
||||
|
||||
def test_about_page(self):
|
||||
""" Get URL for about page, no marketing site """
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
|
||||
self.assertEquals(self.get_about_page_link(), "http://localhost:8000/courses/mitX/101/test/about")
|
||||
# create test mongo course
|
||||
self.course = CourseFactory.create(
|
||||
org='test_org',
|
||||
number='test_number',
|
||||
run='test_run',
|
||||
default_store=ModuleStoreEnum.Type.split,
|
||||
social_sharing_url='test_social_sharing_url',
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root'})
|
||||
def test_about_page_marketing_site(self):
|
||||
""" Get URL for about page, marketing root present. """
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
self.assertEquals(self.get_about_page_link(), "https://dummy-root/courses/mitX/101/test/about")
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
|
||||
self.assertEquals(self.get_about_page_link(), "http://localhost:8000/courses/mitX/101/test/about")
|
||||
# load this course into course overview and set it's marketing url
|
||||
self.course_overview = CourseOverview.get_from_id(self.course.id)
|
||||
self.course_overview.marketing_url = 'test_marketing_url'
|
||||
self.course_overview.save()
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummyhttps://x'})
|
||||
def test_about_page_marketing_site_https__edge(self):
|
||||
""" Get URL for about page """
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
self.assertEquals(self.get_about_page_link(), "https://www.dummyhttps://x/courses/mitX/101/test/about")
|
||||
def get_course_sharing_link(self, enable_social_sharing, enable_mktg_site, use_overview=True):
|
||||
"""
|
||||
Get course sharing link.
|
||||
|
||||
def get_about_page_link(self):
|
||||
""" create mock course and return the about page link."""
|
||||
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
|
||||
return get_lms_link_for_about_page(course_key)
|
||||
Arguments:
|
||||
enable_social_sharing(Boolean): To indicate whether social sharing is enabled.
|
||||
enable_mktg_site(Boolean): A feature flag to decide activation of marketing site.
|
||||
|
||||
Keyword Arguments:
|
||||
use_overview: indicates whether course overview or course descriptor should get
|
||||
past to get_link_for_about_page.
|
||||
|
||||
Returns course sharing url.
|
||||
"""
|
||||
mock_settings = {
|
||||
'FEATURES': {
|
||||
'ENABLE_MKTG_SITE': enable_mktg_site
|
||||
},
|
||||
'SOCIAL_SHARING_SETTINGS': {
|
||||
'CUSTOM_COURSE_URLS': enable_social_sharing
|
||||
},
|
||||
}
|
||||
|
||||
with mock.patch.multiple('django.conf.settings', **mock_settings):
|
||||
course_sharing_link = get_link_for_about_page(
|
||||
self.course_overview if use_overview else self.course
|
||||
)
|
||||
|
||||
return course_sharing_link
|
||||
|
||||
@ddt.data(
|
||||
(True, True, 'test_social_sharing_url'),
|
||||
(False, True, 'test_marketing_url'),
|
||||
(True, False, 'test_social_sharing_url'),
|
||||
(False, False, '{}/courses/course-v1:test_org+test_number+test_run/about'.format(settings.LMS_ROOT_URL)),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_sharing_link_with_settings(self, enable_social_sharing, enable_mktg_site, expected_course_sharing_link):
|
||||
"""
|
||||
Verify the method gives correct course sharing url on settings manipulations.
|
||||
"""
|
||||
actual_course_sharing_link = self.get_course_sharing_link(
|
||||
enable_social_sharing=enable_social_sharing,
|
||||
enable_mktg_site=enable_mktg_site,
|
||||
)
|
||||
self.assertEqual(actual_course_sharing_link, expected_course_sharing_link)
|
||||
|
||||
@ddt.data(
|
||||
(['social_sharing_url'], 'test_marketing_url'),
|
||||
(['marketing_url'], 'test_social_sharing_url'),
|
||||
(
|
||||
['social_sharing_url', 'marketing_url'],
|
||||
'{}/courses/course-v1:test_org+test_number+test_run/about'.format(settings.LMS_ROOT_URL)
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_sharing_link_with_course_overview_attrs(self, overview_attrs, expected_course_sharing_link):
|
||||
"""
|
||||
Verify the method gives correct course sharing url when:
|
||||
1. Neither marketing url nor social sharing url is set.
|
||||
2. Either marketing url or social sharing url is set.
|
||||
"""
|
||||
for overview_attr in overview_attrs:
|
||||
setattr(self.course_overview, overview_attr, None)
|
||||
self.course_overview.save()
|
||||
|
||||
actual_course_sharing_link = self.get_course_sharing_link(
|
||||
enable_social_sharing=True,
|
||||
enable_mktg_site=True,
|
||||
)
|
||||
self.assertEqual(actual_course_sharing_link, expected_course_sharing_link)
|
||||
|
||||
@ddt.data(
|
||||
(True, 'test_social_sharing_url'),
|
||||
(
|
||||
False,
|
||||
'{}/courses/course-v1:test_org+test_number+test_run/about'.format(settings.LMS_ROOT_URL)
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_sharing_link_with_course_descriptor(self, enable_social_sharing, expected_course_sharing_link):
|
||||
"""
|
||||
Verify the method gives correct course sharing url on passing
|
||||
course descriptor as a parameter.
|
||||
"""
|
||||
actual_course_sharing_link = self.get_course_sharing_link(
|
||||
enable_social_sharing=enable_social_sharing,
|
||||
enable_mktg_site=True,
|
||||
use_overview=False,
|
||||
)
|
||||
self.assertEqual(actual_course_sharing_link, expected_course_sharing_link)
|
||||
|
||||
@@ -8,7 +8,7 @@ from rest_framework.reverse import reverse
|
||||
from certificates.api import certificate_downloadable_status
|
||||
from courseware.access import has_access
|
||||
from student.models import CourseEnrollment, User
|
||||
from util.course import get_lms_link_for_about_page
|
||||
from util.course import get_link_for_about_page
|
||||
|
||||
|
||||
class CourseOverviewField(serializers.RelatedField):
|
||||
@@ -52,7 +52,7 @@ class CourseOverviewField(serializers.RelatedField):
|
||||
}
|
||||
},
|
||||
'course_image': course_overview.course_image_url,
|
||||
'course_about': get_lms_link_for_about_page(CourseKey.from_string(course_id)),
|
||||
'course_about': get_link_for_about_page(course_overview),
|
||||
'course_updates': reverse(
|
||||
'course-updates-list',
|
||||
kwargs={'course_id': course_id},
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Sync course runs from catalog service.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.catalog.utils import get_course_runs
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Purpose is to sync course runs data from catalog service to make it accessible in edx-platform.
|
||||
It just happens to only be syncing marketing URLs from catalog course runs for now.
|
||||
"""
|
||||
help = 'Refresh marketing urls from catalog service.'
|
||||
|
||||
def update_course_overviews(self, course_runs):
|
||||
"""
|
||||
Refresh marketing urls for the given catalog course runs.
|
||||
|
||||
Arguments:
|
||||
course_runs: A list containing catalog course runs.
|
||||
"""
|
||||
# metrics for observability
|
||||
# number of catalog course runs retrieved.
|
||||
catalog_course_runs_retrieved = len(course_runs)
|
||||
# number of catalog course runs found in course overview.
|
||||
course_runs_found_in_cache = 0
|
||||
# number of course overview records actually get updated.
|
||||
course_metadata_updated = 0
|
||||
|
||||
for course_run in course_runs:
|
||||
marketing_url = course_run['marketing_url']
|
||||
course_key = CourseKey.from_string(course_run['key'])
|
||||
try:
|
||||
course_overview = CourseOverview.objects.get(id=course_key)
|
||||
course_runs_found_in_cache += 1
|
||||
except CourseOverview.DoesNotExist:
|
||||
log.info(
|
||||
'[sync_course_runs] course overview record not found for course run: %s',
|
||||
unicode(course_key),
|
||||
)
|
||||
continue
|
||||
|
||||
# Check whether course overview's marketing url is outdated - this saves a db hit.
|
||||
if course_overview.marketing_url != marketing_url:
|
||||
course_overview.marketing_url = marketing_url
|
||||
course_overview.save()
|
||||
course_metadata_updated += 1
|
||||
|
||||
return catalog_course_runs_retrieved, course_runs_found_in_cache, course_metadata_updated
|
||||
|
||||
def handle(self, *args, **options):
|
||||
log.info('[sync_course_runs] Fetching course runs from catalog service.')
|
||||
course_runs = get_course_runs()
|
||||
course_runs_retrieved, course_runs_found, course_metadata_updated = self.update_course_overviews(course_runs)
|
||||
|
||||
log.info(
|
||||
('[sync_course_runs] course runs retrieved: %d, course runs found in course overview: %d,'
|
||||
' course runs not found in course overview: %d, course overviews metadata updated: %d,'),
|
||||
course_runs_retrieved,
|
||||
course_runs_found,
|
||||
course_runs_retrieved - course_runs_found,
|
||||
course_metadata_updated,
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Tests for the sync course runs management command.
|
||||
"""
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
COMMAND_MODULE = 'openedx.core.djangoapps.catalog.management.commands.sync_course_runs'
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@mock.patch(COMMAND_MODULE + '.get_course_runs')
|
||||
class TestSyncCourseRunsCommand(ModuleStoreTestCase):
|
||||
"""
|
||||
Test for the sync course runs management command.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestSyncCourseRunsCommand, self).setUp()
|
||||
# create mongo course
|
||||
self.course = CourseFactory.create()
|
||||
# load this course into course overview
|
||||
CourseOverview.get_from_id(self.course.id)
|
||||
# create a catalog course run with the same course id.
|
||||
self.catalog_course_run = CourseRunFactory(
|
||||
key=unicode(self.course.id),
|
||||
marketing_url='test_marketing_url'
|
||||
)
|
||||
|
||||
def get_course_overview_marketing_url(self, course_id):
|
||||
"""
|
||||
Get course overview marketing url.
|
||||
"""
|
||||
return CourseOverview.objects.get(id=course_id).marketing_url
|
||||
|
||||
def test_marketing_url_on_sync(self, mock_catalog_course_runs):
|
||||
"""
|
||||
Verify the updated marketing url on execution of the management command.
|
||||
"""
|
||||
mock_catalog_course_runs.return_value = [self.catalog_course_run]
|
||||
earlier_marketing_url = self.get_course_overview_marketing_url(self.course.id)
|
||||
|
||||
call_command('sync_course_runs')
|
||||
updated_marketing_url = self.get_course_overview_marketing_url(self.course.id)
|
||||
# Assert that the Marketing URL has changed.
|
||||
self.assertNotEqual(earlier_marketing_url, updated_marketing_url)
|
||||
self.assertEqual(updated_marketing_url, 'test_marketing_url')
|
||||
|
||||
@mock.patch(COMMAND_MODULE + '.log.info')
|
||||
def test_course_overview_does_not_exist(self, mock_log_info, mock_catalog_course_runs):
|
||||
"""
|
||||
Verify no error in case if a course run is not found in course overview.
|
||||
"""
|
||||
nonexistent_course_run = CourseRunFactory()
|
||||
mock_catalog_course_runs.return_value = [self.catalog_course_run, nonexistent_course_run]
|
||||
|
||||
call_command('sync_course_runs')
|
||||
|
||||
mock_log_info.assert_any_call(
|
||||
'[sync_course_runs] course overview record not found for course run: %s',
|
||||
nonexistent_course_run['key'],
|
||||
)
|
||||
updated_marketing_url = self.get_course_overview_marketing_url(self.course.id)
|
||||
self.assertEqual(updated_marketing_url, 'test_marketing_url')
|
||||
|
||||
@mock.patch(COMMAND_MODULE + '.log.info')
|
||||
def test_starting_and_ending_logs(self, mock_log_info, mock_catalog_course_runs):
|
||||
"""
|
||||
Verify logging at start and end of the command.
|
||||
"""
|
||||
mock_catalog_course_runs.return_value = [self.catalog_course_run, CourseRunFactory(), CourseRunFactory()]
|
||||
|
||||
call_command('sync_course_runs')
|
||||
# Assert the logs at the start of the command.
|
||||
mock_log_info.assert_any_call('[sync_course_runs] Fetching course runs from catalog service.')
|
||||
# Assert the log metrics at it's completion.
|
||||
mock_log_info.assert_any_call(
|
||||
('[sync_course_runs] course runs retrieved: %d, course runs found in course overview: %d,'
|
||||
' course runs not found in course overview: %d, course overviews metadata updated: %d,'),
|
||||
3,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('catalog', '0002_catalogintegration_username'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='catalogintegration',
|
||||
name='page_size',
|
||||
field=models.PositiveIntegerField(default=100, help_text='Maximum number of records in paginated response of a single request to catalog service.', verbose_name='Page Size'),
|
||||
),
|
||||
]
|
||||
@@ -35,6 +35,14 @@ class CatalogIntegration(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
page_size = models.PositiveIntegerField(
|
||||
verbose_name=_('Page Size'),
|
||||
default=100,
|
||||
help_text=_(
|
||||
'Maximum number of records in paginated response of a single request to catalog service.'
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_cache_enabled(self):
|
||||
"""Whether responses from the catalog API will be cached."""
|
||||
|
||||
@@ -10,6 +10,7 @@ class CatalogIntegrationMixin(object):
|
||||
'internal_api_url': 'https://catalog-internal.example.com/api/v1/',
|
||||
'cache_ttl': 0,
|
||||
'service_username': 'lms_catalog_service_user',
|
||||
'page_size': 20,
|
||||
}
|
||||
|
||||
def create_catalog_integration(self, **kwargs):
|
||||
|
||||
@@ -8,12 +8,13 @@ from django.test import TestCase
|
||||
import mock
|
||||
|
||||
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, ProgramTypeFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory, ProgramTypeFactory
|
||||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||
from openedx.core.djangoapps.catalog.utils import (
|
||||
get_programs,
|
||||
get_program_types,
|
||||
get_programs_with_type,
|
||||
get_course_runs,
|
||||
)
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -176,3 +177,73 @@ class TestGetProgramTypes(CatalogIntegrationMixin, TestCase):
|
||||
program = program_types[0]
|
||||
data = get_program_types(name=program['name'])
|
||||
self.assertEqual(data, program)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@mock.patch(UTILS_MODULE + '.get_edx_api_data')
|
||||
class TestGetCourseRuns(CatalogIntegrationMixin, TestCase):
|
||||
"""
|
||||
Tests covering retrieval of course runs from the catalog service.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestGetCourseRuns, self).setUp()
|
||||
|
||||
self.catalog_integration = self.create_catalog_integration(cache_ttl=1)
|
||||
self.user = UserFactory(username=self.catalog_integration.service_username)
|
||||
|
||||
def assert_contract(self, call_args): # pylint: disable=redefined-builtin
|
||||
"""
|
||||
Verify that API data retrieval utility is used correctly.
|
||||
"""
|
||||
args, kwargs = call_args
|
||||
|
||||
for arg in (self.catalog_integration, self.user, 'course_runs'):
|
||||
self.assertIn(arg, args)
|
||||
|
||||
self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.internal_api_url) # pylint: disable=protected-access
|
||||
|
||||
querystring = {
|
||||
'page_size': 20,
|
||||
'exclude_utm': 1,
|
||||
}
|
||||
|
||||
self.assertEqual(kwargs['querystring'], querystring)
|
||||
|
||||
return args, kwargs
|
||||
|
||||
def test_config_missing(self, mock_get_edx_api_data):
|
||||
"""
|
||||
Verify that no errors occur when catalog config is missing.
|
||||
"""
|
||||
CatalogIntegration.objects.all().delete()
|
||||
|
||||
data = get_course_runs()
|
||||
self.assertFalse(mock_get_edx_api_data.called)
|
||||
self.assertEqual(data, [])
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.log.error')
|
||||
def test_service_user_missing(self, mock_log_error, mock_get_edx_api_data):
|
||||
"""
|
||||
Verify that no errors occur when the catalog service user is missing.
|
||||
"""
|
||||
catalog_integration = self.create_catalog_integration(service_username='nonexistent-user')
|
||||
|
||||
data = get_course_runs()
|
||||
mock_log_error.any_call(
|
||||
'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.',
|
||||
catalog_integration.service_username,
|
||||
)
|
||||
self.assertFalse(mock_get_edx_api_data.called)
|
||||
self.assertEqual(data, [])
|
||||
|
||||
def test_get_course_runs(self, mock_get_edx_api_data):
|
||||
"""
|
||||
Test retrieval of course runs.
|
||||
"""
|
||||
catalog_course_runs = [CourseRunFactory() for __ in xrange(10)]
|
||||
mock_get_edx_api_data.return_value = catalog_course_runs
|
||||
|
||||
data = get_course_runs()
|
||||
self.assertTrue(mock_get_edx_api_data.called)
|
||||
self.assert_contract(mock_get_edx_api_data.call_args)
|
||||
self.assertEqual(data, catalog_course_runs)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Helper functions for working with the catalog service."""
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -10,6 +11,8 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data
|
||||
from openedx.core.lib.token_utils import JwtBuilder
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
User = get_user_model() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
@@ -135,3 +138,40 @@ def get_programs_with_type(types=None):
|
||||
programs_with_type.append(program_with_type)
|
||||
|
||||
return programs_with_type
|
||||
|
||||
|
||||
def get_course_runs():
|
||||
"""
|
||||
Retrieve all the course runs from the catalog service.
|
||||
|
||||
Returns:
|
||||
list of dict with each record representing a course run.
|
||||
"""
|
||||
catalog_integration = CatalogIntegration.current()
|
||||
course_runs = []
|
||||
if catalog_integration.enabled:
|
||||
try:
|
||||
user = User.objects.get(username=catalog_integration.service_username)
|
||||
except User.DoesNotExist:
|
||||
log.error(
|
||||
'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.',
|
||||
catalog_integration.service_username,
|
||||
)
|
||||
return course_runs
|
||||
|
||||
api = create_catalog_api_client(user, catalog_integration)
|
||||
|
||||
querystring = {
|
||||
'page_size': catalog_integration.page_size,
|
||||
'exclude_utm': 1,
|
||||
}
|
||||
|
||||
course_runs = get_edx_api_data(
|
||||
catalog_integration,
|
||||
user,
|
||||
'course_runs',
|
||||
api=api,
|
||||
querystring=querystring,
|
||||
)
|
||||
|
||||
return course_runs
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('course_overviews', '0010_auto_20160329_2317'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='courseoverview',
|
||||
name='marketing_url',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -97,6 +97,7 @@ class CourseOverview(TimeStampedModel):
|
||||
course_video_url = TextField(null=True)
|
||||
effort = TextField(null=True)
|
||||
self_paced = BooleanField(default=False)
|
||||
marketing_url = TextField(null=True)
|
||||
|
||||
@classmethod
|
||||
def _create_from_course(cls, course):
|
||||
|
||||
Reference in New Issue
Block a user