Commit 39e572b9 by Clinton Blackburn

Added subject data loader

ECOM-5190
parent c6bd30f0
...@@ -77,10 +77,8 @@ class DrupalApiDataLoader(AbstractDataLoader): ...@@ -77,10 +77,8 @@ class DrupalApiDataLoader(AbstractDataLoader):
"""Update `course` with subjects from `body`.""" """Update `course` with subjects from `body`."""
course.subjects.clear() course.subjects.clear()
subjects = (s['title'] for s in body['subjects']) subjects = (s['title'] for s in body['subjects'])
for subject_name in subjects: subjects = Subject.objects.filter(name__in=subjects, partner=self.partner)
# Normalize subject names with title case course.subjects.add(*subjects)
subject, __ = Subject.objects.get_or_create(name=subject_name.title())
course.subjects.add(subject)
def set_sponsors(self, course, body): def set_sponsors(self, course, body):
"""Update `course` with sponsors from `body`.""" """Update `course` with sponsors from `body`."""
...@@ -285,3 +283,25 @@ class XSeriesMarketingSiteDataLoader(AbstractMarketingSiteDataLoader): ...@@ -285,3 +283,25 @@ class XSeriesMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
program.save() program.save()
logger.info('Processed XSeries with marketing_slug [%s].', marketing_slug) logger.info('Processed XSeries with marketing_slug [%s].', marketing_slug)
return program return program
class SubjectMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
@property
def node_type(self):
return 'subject'
def process_node(self, data):
slug = data['field_subject_url_slug']
defaults = {
'uuid': data['uuid'],
'name': data['title'],
'description': self.clean_html(data['body']['value']),
'subtitle': self.clean_html(data['field_subject_subtitle']['value']),
'card_image_url': self._get_nested_url(data.get('field_subject_card_image')),
# NOTE (CCB): This is not a typo. Yes, the banner image for subjects is in a field with xseries in the name.
'banner_image_url': self._get_nested_url(data.get('field_xseries_banner_image'))
}
subject, __ = Subject.objects.update_or_create(slug=slug, partner=self.partner, defaults=defaults)
logger.info('Processed subject with slug [%s].', slug)
return subject
import json import json
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from uuid import UUID
import ddt import ddt
import mock import mock
...@@ -8,7 +9,7 @@ from django.test import TestCase ...@@ -8,7 +9,7 @@ from django.test import TestCase
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.data_loaders.marketing_site import ( from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader
) )
from course_discovery.apps.course_metadata.data_loaders.tests import JSON from course_discovery.apps.course_metadata.data_loaders.tests import JSON
from course_discovery.apps.course_metadata.data_loaders.tests.mixins import ApiClientTestMixin, DataLoaderTestMixin from course_discovery.apps.course_metadata.data_loaders.tests.mixins import ApiClientTestMixin, DataLoaderTestMixin
...@@ -55,9 +56,19 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase ...@@ -55,9 +56,19 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase
Person.objects.create(key=mock_data.ORPHAN_STAFF_KEY) Person.objects.create(key=mock_data.ORPHAN_STAFF_KEY)
Organization.objects.create(key=mock_data.ORPHAN_ORGANIZATION_KEY) Organization.objects.create(key=mock_data.ORPHAN_ORGANIZATION_KEY)
def create_mock_subjects(self, course_runs):
course_runs = course_runs['items']
for course_run in course_runs:
if course_run:
for subject in course_run['subjects']:
Subject.objects.get_or_create(name=subject['title'], partner=self.partner)
def mock_api(self): def mock_api(self):
"""Mock out the Drupal API. Returns a list of mocked-out course runs.""" """Mock out the Drupal API. Returns a list of mocked-out course runs."""
body = mock_data.MARKETING_API_BODY body = mock_data.MARKETING_API_BODY
self.create_mock_subjects(body)
responses.add( responses.add(
responses.GET, responses.GET,
self.api_url + 'courses/', self.api_url + 'courses/',
...@@ -111,11 +122,10 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase ...@@ -111,11 +122,10 @@ class DrupalApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCase
def assert_subjects_loaded(self, course, body): def assert_subjects_loaded(self, course, body):
"""Verify that subjects have been loaded correctly.""" """Verify that subjects have been loaded correctly."""
course_subjects = course.subjects.all() course_subjects = course.subjects.all()
api_subjects = body['subjects'] expected_subjects = body['subjects']
self.assertEqual(len(course_subjects), len(api_subjects)) expected_subjects = [subject['title'] for subject in expected_subjects]
for api_subject in api_subjects: actual_subjects = list(course_subjects.values_list('name', flat=True))
loaded_subject = Subject.objects.get(name=api_subject['title'].title()) self.assertEqual(actual_subjects, expected_subjects)
self.assertIn(loaded_subject, course_subjects)
def assert_sponsors_loaded(self, course, body): def assert_sponsors_loaded(self, course, body):
"""Verify that sponsors have been loaded correctly.""" """Verify that sponsors have been loaded correctly."""
...@@ -298,7 +308,6 @@ class AbstractMarketingSiteDataLoaderTestMixin(DataLoaderTestMixin): ...@@ -298,7 +308,6 @@ class AbstractMarketingSiteDataLoaderTestMixin(DataLoaderTestMixin):
class XSeriesMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixin, TestCase): class XSeriesMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixin, TestCase):
loader_class = XSeriesMarketingSiteDataLoader loader_class = XSeriesMarketingSiteDataLoader
LOGIN_COOKIE = ('session_id', 'abc123')
def create_mock_programs(self, programs): def create_mock_programs(self, programs):
for program in programs: for program in programs:
...@@ -364,3 +373,45 @@ class XSeriesMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMix ...@@ -364,3 +373,45 @@ class XSeriesMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMix
calls = [mock.call('Program [%s] exists on the marketing site, but not in the Programs Service!', calls = [mock.call('Program [%s] exists on the marketing site, but not in the Programs Service!',
datum['url'].split('/')[-1]) for datum in api_data] datum['url'].split('/')[-1]) for datum in api_data]
mock_logger.error.assert_has_calls(calls) mock_logger.error.assert_has_calls(calls)
class SubjectMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixin, TestCase):
loader_class = SubjectMarketingSiteDataLoader
def mock_api(self):
bodies = mock_data.MARKETING_SITE_API_SUBJECT_BODIES
url = self.api_url + 'node.json'
responses.add_callback(
responses.GET,
url,
callback=self.mock_api_callback(url, bodies),
content_type=JSON
)
return bodies
def assert_subject_loaded(self, data):
slug = data['field_subject_url_slug']
subject = Subject.objects.get(slug=slug, partner=self.partner)
expected_values = {
'uuid': UUID(data['uuid']),
'name': data['title'],
'description': self.loader.clean_html(data['body']['value']),
'subtitle': self.loader.clean_html(data['field_subject_subtitle']['value']),
'card_image_url': data['field_subject_card_image']['url'],
'banner_image_url': data['field_xseries_banner_image']['url'],
}
for field, value in expected_values.items():
self.assertEqual(getattr(subject, field), value)
@responses.activate
def test_ingest(self):
self.mock_login_response()
api_data = self.mock_api()
self.loader.ingest()
for datum in api_data:
self.assert_subject_loaded(datum)
...@@ -8,7 +8,7 @@ from course_discovery.apps.course_metadata.data_loaders.api import ( ...@@ -8,7 +8,7 @@ from course_discovery.apps.course_metadata.data_loaders.api import (
CoursesApiDataLoader, OrganizationsApiDataLoader, EcommerceApiDataLoader, ProgramsApiDataLoader, CoursesApiDataLoader, OrganizationsApiDataLoader, EcommerceApiDataLoader, ProgramsApiDataLoader,
) )
from course_discovery.apps.course_metadata.data_loaders.marketing_site import ( from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -78,6 +78,7 @@ class Command(BaseCommand): ...@@ -78,6 +78,7 @@ class Command(BaseCommand):
raise raise
data_loaders = ( data_loaders = (
(partner.marketing_site_url_root, SubjectMarketingSiteDataLoader,),
(partner.organizations_api_url, OrganizationsApiDataLoader,), (partner.organizations_api_url, OrganizationsApiDataLoader,),
(partner.courses_api_url, CoursesApiDataLoader,), (partner.courses_api_url, CoursesApiDataLoader,),
(partner.ecommerce_api_url, EcommerceApiDataLoader,), (partner.ecommerce_api_url, EcommerceApiDataLoader,),
......
...@@ -795,3 +795,52 @@ MARKETING_SITE_API_XSERIES_BODIES = [ ...@@ -795,3 +795,52 @@ MARKETING_SITE_API_XSERIES_BODIES = [
'url': 'https://www.edx.org/xseries/supply-chain-management-0' 'url': 'https://www.edx.org/xseries/supply-chain-management-0'
} }
] ]
MARKETING_SITE_API_SUBJECT_BODIES = [
{
'body': {
'value': 'Yay! CS!',
'summary': '',
'format': 'expanded_html'
},
'field_xseries_banner_image': {
'url': 'https://prod-edx-mktg-edit.edx.org/sites/default/files/cs-1440x210.jpg'
},
'field_subject_url_slug': 'computer-science',
'field_subject_subtitle': {
'value': 'Learn about computer science from the best universities and institutions around the world.',
'format': 'basic_html'
},
'field_subject_card_image': {
'url': 'https://prod-edx-mktg-edit.edx.org/sites/default/files/subject/image/card/computer-science.jpg',
},
'type': 'subject',
'title': 'Computer Science',
'url': 'https://prod-edx-mktg-edit.edx.org/course/subject/math',
'uuid': 'e52e2134-a4e4-4fcb-805f-cbef40812580',
},
{
'body': {
'value': 'Take free online math courses from MIT, Caltech, Tsinghua and other leading math and science '
'institutions. Get introductions to algebra, geometry, trigonometry, precalculus and calculus '
'or get help with current math coursework and AP exam preparation.',
'summary': '',
'format': 'basic_html'
},
'field_xseries_banner_image': {
'url': 'https://prod-edx-mktg-edit.edx.org/sites/default/files/mathemagical-1440x210.jpg',
},
'field_subject_url_slug': 'math',
'field_subject_subtitle': {
'value': 'Learn about math and more from the best universities and institutions around the world.',
'format': 'basic_html'
},
'field_subject_card_image': {
'url': 'https://prod-edx-mktg-edit.edx.org/sites/default/files/subject/image/card/math.jpg',
},
'type': 'subject',
'title': 'Math',
'url': 'https://prod-edx-mktg-edit.edx.org/course/subject/math',
'uuid': 'a669e004-cbc0-4b68-8882-234c12e1cce4',
},
]
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