Sync course language: Discovery Course Catalog -> LMS Course Overview
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Sync course runs from catalog service.
|
Sync course runs from catalog service.
|
||||||
"""
|
"""
|
||||||
|
from collections import namedtuple
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
@@ -15,33 +16,30 @@ log = logging.getLogger(__name__)
|
|||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Purpose is to sync course runs data from catalog service to make it accessible in edx-platform.
|
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.'
|
help = 'Refresh marketing urls from catalog service.'
|
||||||
|
|
||||||
def update_course_overviews(self, course_runs):
|
CourseRunField = namedtuple('CourseRunField', 'catalog_name course_overview_name')
|
||||||
"""
|
course_run_fields = (
|
||||||
Refresh marketing urls for the given catalog course runs.
|
CourseRunField(catalog_name='marketing_url', course_overview_name='marketing_url'),
|
||||||
|
CourseRunField(catalog_name='eligible_for_financial_aid', course_overview_name='eligible_for_financial_aid'),
|
||||||
|
CourseRunField(catalog_name='content_language', course_overview_name='language'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
log.info('[sync_course_runs] Fetching course runs from catalog service.')
|
||||||
|
course_runs = get_course_runs()
|
||||||
|
|
||||||
Arguments:
|
|
||||||
course_runs: A list containing catalog course runs.
|
|
||||||
"""
|
|
||||||
# metrics for observability
|
# metrics for observability
|
||||||
# number of catalog course runs retrieved.
|
num_runs_found_in_catalog = len(course_runs)
|
||||||
catalog_course_runs_retrieved = len(course_runs)
|
num_runs_found_in_course_overview = 0
|
||||||
# number of catalog course runs found in course overview.
|
num_course_overviews_updated = 0
|
||||||
course_runs_found_in_cache = 0
|
|
||||||
# number of course overview records actually get updated.
|
|
||||||
course_metadata_updated = 0
|
|
||||||
|
|
||||||
for course_run in course_runs:
|
for course_run in course_runs:
|
||||||
is_course_metadata_updated = False
|
|
||||||
marketing_url = course_run['marketing_url']
|
|
||||||
eligible_for_financial_aid = course_run['eligible_for_financial_aid']
|
|
||||||
course_key = CourseKey.from_string(course_run['key'])
|
course_key = CourseKey.from_string(course_run['key'])
|
||||||
try:
|
try:
|
||||||
course_overview = CourseOverview.objects.get(id=course_key)
|
course_overview = CourseOverview.objects.get(id=course_key)
|
||||||
course_runs_found_in_cache += 1
|
num_runs_found_in_course_overview += 1
|
||||||
except CourseOverview.DoesNotExist:
|
except CourseOverview.DoesNotExist:
|
||||||
log.info(
|
log.info(
|
||||||
'[sync_course_runs] course overview record not found for course run: %s',
|
'[sync_course_runs] course overview record not found for course run: %s',
|
||||||
@@ -49,32 +47,25 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check whether course overview's marketing url is outdated - this saves a db hit.
|
is_course_metadata_updated = False
|
||||||
if course_overview.marketing_url != marketing_url:
|
for field in self.course_run_fields:
|
||||||
course_overview.marketing_url = marketing_url
|
catalog_value = course_run.get(field.catalog_name)
|
||||||
is_course_metadata_updated = True
|
if getattr(course_overview, field.course_overview_name) != catalog_value:
|
||||||
|
setattr(course_overview, field.course_overview_name, catalog_value)
|
||||||
# Check whether course overview's eligible for financial aid is outdated
|
is_course_metadata_updated = True
|
||||||
if course_overview.eligible_for_financial_aid != eligible_for_financial_aid:
|
|
||||||
course_overview.eligible_for_financial_aid = eligible_for_financial_aid
|
|
||||||
is_course_metadata_updated = True
|
|
||||||
|
|
||||||
if is_course_metadata_updated:
|
if is_course_metadata_updated:
|
||||||
course_overview.save()
|
course_overview.save()
|
||||||
course_metadata_updated += 1
|
num_course_overviews_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(
|
log.info(
|
||||||
('[sync_course_runs] course runs retrieved: %d, course runs found in course overview: %d,'
|
'[sync_course_runs] '
|
||||||
' course runs not found in course overview: %d, course overviews metadata updated: %d,'),
|
'course runs found in catalog: %d, '
|
||||||
course_runs_retrieved,
|
'course runs found in course overview: %d, '
|
||||||
course_runs_found,
|
'course runs not found in course overview: %d, '
|
||||||
course_runs_retrieved - course_runs_found,
|
'course overviews updated: %d',
|
||||||
course_metadata_updated,
|
num_runs_found_in_catalog,
|
||||||
|
num_runs_found_in_course_overview,
|
||||||
|
num_runs_found_in_catalog - num_runs_found_in_course_overview,
|
||||||
|
num_course_overviews_updated,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import mock
|
|||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
|
||||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory
|
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory
|
||||||
|
from openedx.core.djangoapps.catalog.management.commands.sync_course_runs import Command as sync_command
|
||||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||||
from xmodule.modulestore.tests.factories import CourseFactory
|
from xmodule.modulestore.tests.factories import CourseFactory
|
||||||
@@ -25,7 +26,7 @@ class TestSyncCourseRunsCommand(ModuleStoreTestCase):
|
|||||||
# create mongo course
|
# create mongo course
|
||||||
self.course = CourseFactory.create()
|
self.course = CourseFactory.create()
|
||||||
# load this course into course overview
|
# load this course into course overview
|
||||||
CourseOverview.get_from_id(self.course.id)
|
self.course_overview = CourseOverview.get_from_id(self.course.id)
|
||||||
# create a catalog course run with the same course id.
|
# create a catalog course run with the same course id.
|
||||||
self.catalog_course_run = CourseRunFactory(
|
self.catalog_course_run = CourseRunFactory(
|
||||||
key=unicode(self.course.id),
|
key=unicode(self.course.id),
|
||||||
@@ -33,31 +34,34 @@ class TestSyncCourseRunsCommand(ModuleStoreTestCase):
|
|||||||
eligible_for_financial_aid=False
|
eligible_for_financial_aid=False
|
||||||
)
|
)
|
||||||
|
|
||||||
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_course_run_sync(self, mock_catalog_course_runs):
|
def test_course_run_sync(self, mock_catalog_course_runs):
|
||||||
"""
|
"""
|
||||||
Verify on executing management command course overview data is updated
|
Verify on executing management command course overview data is updated
|
||||||
with course run data from course discovery.
|
with course run data from course discovery.
|
||||||
"""
|
"""
|
||||||
mock_catalog_course_runs.return_value = [self.catalog_course_run]
|
mock_catalog_course_runs.return_value = [self.catalog_course_run]
|
||||||
earlier_marketing_url = self.get_course_overview_marketing_url(self.course.id)
|
|
||||||
course_overview = CourseOverview.objects.get(id=self.course.id)
|
|
||||||
earlier_eligible_for_financial_aid = course_overview.eligible_for_financial_aid
|
|
||||||
|
|
||||||
call_command('sync_course_runs')
|
call_command('sync_course_runs')
|
||||||
course_overview.refresh_from_db()
|
updated_course_overview = CourseOverview.objects.get(id=self.course.id)
|
||||||
updated_marketing_url = self.get_course_overview_marketing_url(self.course.id)
|
|
||||||
updated_eligible_for_financial_aid = course_overview.eligible_for_financial_aid
|
# assert fields have updated
|
||||||
# Assert that the Marketing URL has changed.
|
for field in sync_command.course_run_fields:
|
||||||
self.assertNotEqual(earlier_marketing_url, updated_marketing_url)
|
course_overview_field_name = field.course_overview_name
|
||||||
self.assertNotEqual(earlier_eligible_for_financial_aid, updated_eligible_for_financial_aid)
|
catalog_field_name = field.catalog_name
|
||||||
self.assertEqual(updated_marketing_url, 'test_marketing_url')
|
|
||||||
self.assertEqual(updated_eligible_for_financial_aid, False)
|
previous_course_overview_value = getattr(self.course_overview, course_overview_field_name),
|
||||||
|
updated_course_overview_value = getattr(updated_course_overview, course_overview_field_name)
|
||||||
|
|
||||||
|
# course overview value matches catalog value
|
||||||
|
self.assertEqual(
|
||||||
|
updated_course_overview_value,
|
||||||
|
self.catalog_course_run.get(catalog_field_name),
|
||||||
|
)
|
||||||
|
# new value doesn't match old value
|
||||||
|
self.assertNotEqual(
|
||||||
|
updated_course_overview_value,
|
||||||
|
previous_course_overview_value,
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch(COMMAND_MODULE + '.log.info')
|
@mock.patch(COMMAND_MODULE + '.log.info')
|
||||||
def test_course_overview_does_not_exist(self, mock_log_info, mock_catalog_course_runs):
|
def test_course_overview_does_not_exist(self, mock_log_info, mock_catalog_course_runs):
|
||||||
@@ -73,7 +77,7 @@ class TestSyncCourseRunsCommand(ModuleStoreTestCase):
|
|||||||
'[sync_course_runs] course overview record not found for course run: %s',
|
'[sync_course_runs] course overview record not found for course run: %s',
|
||||||
nonexistent_course_run['key'],
|
nonexistent_course_run['key'],
|
||||||
)
|
)
|
||||||
updated_marketing_url = self.get_course_overview_marketing_url(self.course.id)
|
updated_marketing_url = CourseOverview.objects.get(id=self.course.id).marketing_url
|
||||||
self.assertEqual(updated_marketing_url, 'test_marketing_url')
|
self.assertEqual(updated_marketing_url, 'test_marketing_url')
|
||||||
|
|
||||||
@mock.patch(COMMAND_MODULE + '.log.info')
|
@mock.patch(COMMAND_MODULE + '.log.info')
|
||||||
@@ -81,17 +85,22 @@ class TestSyncCourseRunsCommand(ModuleStoreTestCase):
|
|||||||
"""
|
"""
|
||||||
Verify logging at start and end of the command.
|
Verify logging at start and end of the command.
|
||||||
"""
|
"""
|
||||||
|
def _assert_logs(num_updates):
|
||||||
|
mock_log_info.assert_any_call('[sync_course_runs] Fetching course runs from catalog service.')
|
||||||
|
mock_log_info.assert_any_call(
|
||||||
|
'[sync_course_runs] course runs found in catalog: %d, course runs found in course overview: %d,'
|
||||||
|
' course runs not found in course overview: %d, course overviews updated: %d',
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
num_updates,
|
||||||
|
)
|
||||||
|
mock_log_info.reset_mock()
|
||||||
|
|
||||||
mock_catalog_course_runs.return_value = [self.catalog_course_run, CourseRunFactory(), CourseRunFactory()]
|
mock_catalog_course_runs.return_value = [self.catalog_course_run, CourseRunFactory(), CourseRunFactory()]
|
||||||
|
|
||||||
call_command('sync_course_runs')
|
call_command('sync_course_runs')
|
||||||
# Assert the logs at the start of the command.
|
_assert_logs(num_updates=1)
|
||||||
mock_log_info.assert_any_call('[sync_course_runs] Fetching course runs from catalog service.')
|
|
||||||
# Assert the log metrics at it's completion.
|
call_command('sync_course_runs')
|
||||||
mock_log_info.assert_any_call(
|
_assert_logs(num_updates=0)
|
||||||
('[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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from opaque_keys.edx.keys import CourseKey
|
|||||||
|
|
||||||
from config_models.models import ConfigurationModel
|
from config_models.models import ConfigurationModel
|
||||||
from lms.djangoapps import django_comment_client
|
from lms.djangoapps import django_comment_client
|
||||||
|
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||||
from static_replace.models import AssetBaseUrlConfig
|
from static_replace.models import AssetBaseUrlConfig
|
||||||
from xmodule import course_metadata_utils, block_metadata_utils
|
from xmodule import course_metadata_utils, block_metadata_utils
|
||||||
@@ -194,7 +195,8 @@ class CourseOverview(TimeStampedModel):
|
|||||||
course_overview.course_video_url = CourseDetails.fetch_video_url(course.id)
|
course_overview.course_video_url = CourseDetails.fetch_video_url(course.id)
|
||||||
course_overview.self_paced = course.self_paced
|
course_overview.self_paced = course.self_paced
|
||||||
|
|
||||||
course_overview.language = course.language
|
if not CatalogIntegration.is_enabled():
|
||||||
|
course_overview.language = course.language
|
||||||
|
|
||||||
return course_overview
|
return course_overview
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from django.utils import timezone
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from lms.djangoapps.certificates.api import get_active_web_certificate
|
from lms.djangoapps.certificates.api import get_active_web_certificate
|
||||||
|
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||||
from openedx.core.lib.courses import course_image_url
|
from openedx.core.lib.courses import course_image_url
|
||||||
from static_replace.models import AssetBaseUrlConfig
|
from static_replace.models import AssetBaseUrlConfig
|
||||||
@@ -40,7 +41,7 @@ from ..models import CourseOverview, CourseOverviewImageSet, CourseOverviewImage
|
|||||||
|
|
||||||
@attr(shard=3)
|
@attr(shard=3)
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class CourseOverviewTestCase(ModuleStoreTestCase):
|
class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase):
|
||||||
"""
|
"""
|
||||||
Tests for CourseOverview model.
|
Tests for CourseOverview model.
|
||||||
"""
|
"""
|
||||||
@@ -270,6 +271,24 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
|||||||
course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **kwargs)
|
course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **kwargs)
|
||||||
self.check_course_overview_against_course(course)
|
self.check_course_overview_against_course(course)
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
|
def test_language_field(self, catalog_integration_enabled):
|
||||||
|
"""
|
||||||
|
Test that the language field is not updated from the modulestore
|
||||||
|
when catalog integration is enabled. In that case, it gets updated
|
||||||
|
by the sync_course_runs management command, which synchronizes with
|
||||||
|
the Catalog service.
|
||||||
|
"""
|
||||||
|
self.create_catalog_integration(enabled=catalog_integration_enabled)
|
||||||
|
|
||||||
|
course = CourseFactory.create(language='en')
|
||||||
|
course_overview = CourseOverview.get_from_id(course.id)
|
||||||
|
|
||||||
|
if catalog_integration_enabled:
|
||||||
|
self.assertNotEqual(course_overview.language, course.language)
|
||||||
|
else:
|
||||||
|
self.assertEqual(course_overview.language, course.language)
|
||||||
|
|
||||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||||
def test_get_non_existent_course(self, modulestore_type):
|
def test_get_non_existent_course(self, modulestore_type):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user