Commit 1dd3681b by Clinton Blackburn Committed by Clinton Blackburn

Updated course and course run data models and data loaders

ECOM-5195 and ECOM-5196
parent da420c53
......@@ -24,3 +24,17 @@ class StdImageSerializerField(serializers.Field):
def to_internal_value(self, obj):
""" We do not need to save/edit this banner image through serializer yet """
pass
class ImageField(serializers.Field): # pylint:disable=abstract-method
""" This field mimics the format of `ImageSerializer`. It is intended to aid the transition away from the
`Image` model to simple URLs.
"""
def to_representation(self, value):
return {
'src': value,
'description': None,
'height': None,
'width': None
}
......@@ -9,7 +9,7 @@ from rest_framework import serializers
from rest_framework.fields import DictField
from taggit_serializer.serializers import TagListSerializerField, TaggitSerializer
from course_discovery.apps.api.fields import StdImageSerializerField
from course_discovery.apps.api.fields import StdImageSerializerField, ImageField
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.course_metadata.models import (
Course, CourseRun, Image, Organization, Person, Prerequisite, Seat, Subject, Video, Program, ProgramType,
......@@ -210,10 +210,10 @@ class CourseRunSerializer(TimestampModelSerializer):
help_text=_('Language in which the course is administered')
)
transcript_languages = serializers.SlugRelatedField(many=True, read_only=True, slug_field='code')
image = ImageSerializer()
image = ImageField(read_only=True, source='card_image_url')
video = VideoSerializer()
seats = SeatSerializer(many=True)
instructors = PersonSerializer(many=True)
instructors = serializers.SerializerMethodField(help_text='This field is deprecated. Use staff.')
staff = PersonSerializer(many=True)
marketing_url = serializers.SerializerMethodField()
level_type = serializers.SlugRelatedField(read_only=True, slug_field='name')
......@@ -230,6 +230,9 @@ class CourseRunSerializer(TimestampModelSerializer):
def get_marketing_url(self, obj):
return get_marketing_url_for_user(self.context['request'].user, obj.marketing_url)
def get_instructors(self, obj): # pylint: disable=unused-argument
return []
class CourseRunWithProgramsSerializer(CourseRunSerializer):
"""A ``CourseRunSerializer`` which includes programs derived from parent course."""
......@@ -254,10 +257,10 @@ class CourseSerializer(TimestampModelSerializer):
subjects = SubjectSerializer(many=True)
prerequisites = PrerequisiteSerializer(many=True)
expected_learning_items = serializers.SlugRelatedField(many=True, read_only=True, slug_field='value')
image = ImageSerializer()
image = ImageField(read_only=True, source='card_image_url')
video = VideoSerializer()
owners = OrganizationSerializer(many=True)
sponsors = OrganizationSerializer(many=True)
owners = OrganizationSerializer(many=True, source='authoring_organizations')
sponsors = OrganizationSerializer(many=True, source='sponsoring_organizations')
course_runs = CourseRunSerializer(many=True)
marketing_url = serializers.SerializerMethodField()
......@@ -344,7 +347,7 @@ class AffiliateWindowSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='course_run.title')
desc = serializers.CharField(source='course_run.short_description')
purl = serializers.CharField(source='course_run.marketing_url')
imgurl = serializers.CharField(source='course_run.image')
imgurl = serializers.CharField(source='course_run.card_image_url')
category = serializers.SerializerMethodField()
price = serializers.SerializerMethodField()
......@@ -375,13 +378,14 @@ class FlattenedCourseRunWithCourseSerializer(CourseRunSerializer):
level_type = serializers.SerializerMethodField()
expected_learning_items = serializers.SerializerMethodField()
course_key = serializers.SerializerMethodField()
image = ImageField(read_only=True, source='card_image_url')
class Meta(object):
model = CourseRun
fields = (
'key', 'title', 'short_description', 'full_description', 'level_type', 'subjects', 'prerequisites',
'start', 'end', 'enrollment_start', 'enrollment_end', 'announcement', 'seats', 'content_language',
'transcript_languages', 'instructors', 'staff', 'pacing_type', 'min_effort', 'max_effort', 'course_key',
'transcript_languages', 'staff', 'pacing_type', 'min_effort', 'max_effort', 'course_key',
'expected_learning_items', 'image', 'video', 'owners', 'sponsors', 'modified', 'marketing_url',
)
......@@ -428,10 +432,10 @@ class FlattenedCourseRunWithCourseSerializer(CourseRunSerializer):
return seats
def get_owners(self, obj):
return ','.join([owner.key for owner in obj.course.owners.all()])
return ','.join([owner.key for owner in obj.course.authoring_organizations.all()])
def get_sponsors(self, obj):
return ','.join([sponsor.key for sponsor in obj.course.sponsors.all()])
return ','.join([sponsor.key for sponsor in obj.course.sponsoring_organizations.all()])
def get_subjects(self, obj):
return ','.join([subject.name for subject in obj.course.subjects.all()])
......
from django.test import TestCase
from course_discovery.apps.api.fields import ImageField
class ImageFieldTests(TestCase):
def test_to_representation(self):
value = 'https://example.com/image.jpg'
expected = {
'src': value,
'description': None,
'height': None,
'width': None
}
self.assertEqual(ImageField().to_representation(value), expected)
......@@ -7,6 +7,7 @@ from haystack.query import SearchQuerySet
from opaque_keys.edx.keys import CourseKey
from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.fields import ImageField
from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer,
......@@ -71,7 +72,6 @@ class CatalogSerializerTests(TestCase):
class CourseSerializerTests(TestCase):
def test_data(self):
course = CourseFactory()
image = course.image
video = course.video
request = make_request()
......@@ -88,10 +88,10 @@ class CourseSerializerTests(TestCase):
'subjects': [],
'prerequisites': [],
'expected_learning_items': [],
'image': ImageSerializer(image).data,
'image': ImageField().to_representation(course.card_image_url),
'video': VideoSerializer(video).data,
'owners': [],
'sponsors': [],
'owners': OrganizationSerializer(course.authoring_organizations, many=True).data,
'sponsors': OrganizationSerializer(course.sponsoring_organizations, many=True).data,
'modified': json_date_format(course.modified), # pylint: disable=no-member
'course_runs': CourseRunSerializer(course.course_runs, many=True, context={'request': request}).data,
'marketing_url': '{url}?{params}'.format(
......@@ -106,23 +106,12 @@ class CourseSerializerTests(TestCase):
self.assertDictEqual(serializer.data, expected)
def test_data_url_none(self):
"""
Verify that the course serializer does not attempt to add URL
parameters if the course has no marketing URL.
"""
course = CourseFactory(marketing_url=None)
request = make_request()
serializer = CourseSerializer(course, context={'request': request})
self.assertEqual(serializer.data['marketing_url'], None)
class CourseRunSerializerTests(TestCase):
def test_data(self):
request = make_request()
course_run = CourseRunFactory()
course = course_run.course
image = course_run.image
video = course_run.video
serializer = CourseRunWithProgramsSerializer(course_run, context={'request': request})
ProgramFactory(courses=[course])
......@@ -138,7 +127,7 @@ class CourseRunSerializerTests(TestCase):
'enrollment_start': json_date_format(course_run.enrollment_start),
'enrollment_end': json_date_format(course_run.enrollment_end),
'announcement': json_date_format(course_run.announcement),
'image': ImageSerializer(image).data,
'image': ImageField().to_representation(course_run.card_image_url),
'video': VideoSerializer(video).data,
'pacing_type': course_run.pacing_type,
'content_language': course_run.language.code,
......@@ -163,16 +152,6 @@ class CourseRunSerializerTests(TestCase):
self.assertDictEqual(serializer.data, expected)
def test_data_url_none(self):
"""
Verify that the course run serializer does not attempt to add URL
parameters if the course has no marketing URL.
"""
course_run = CourseRunFactory(marketing_url=None)
request = make_request()
serializer = CourseRunSerializer(course_run, context={'request': request})
self.assertEqual(serializer.data['marketing_url'], None)
class ProgramCourseSerializerTests(TestCase):
def setUp(self):
......@@ -218,35 +197,12 @@ class ProgramCourseSerializerTests(TestCase):
excluded_runs.append(course_runs[0])
program = ProgramFactory(courses=[course], excluded_course_runs=excluded_runs)
serializer = ProgramCourseSerializer(
course,
context={'request': self.request, 'program': program}
)
expected = {
'key': course.key,
'title': course.title,
'short_description': course.short_description,
'full_description': course.full_description,
'level_type': course.level_type.name,
'subjects': [],
'prerequisites': [],
'expected_learning_items': [],
'image': ImageSerializer(course.image).data,
'video': VideoSerializer(course.video).data,
'owners': [],
'sponsors': [],
'modified': json_date_format(course.modified), # pylint: disable=no-member
'course_runs': CourseRunSerializer([course_runs[1]], many=True, context={'request': self.request}).data,
'marketing_url': '{url}?{params}'.format(
url=course.marketing_url,
params=urlencode({
'utm_source': self.request.user.username,
'utm_medium': self.request.user.referral_tracking_id,
})
),
}
serializer_context = {'request': self.request, 'program': program}
serializer = ProgramCourseSerializer(course, context=serializer_context)
expected = CourseSerializer(course, context=serializer_context).data
expected['course_runs'] = CourseRunSerializer([course_runs[1]], many=True,
context={'request': self.request}).data
self.assertDictEqual(serializer.data, expected)
......@@ -496,7 +452,7 @@ class AffiliateWindowSerializerTests(TestCase):
'actualp': seat.price
},
'currency': seat.currency.code,
'imgurl': course_run.image.src,
'imgurl': course_run.card_image_url,
'category': 'Other Experiences'
}
self.assertDictEqual(serializer.data, expected)
......@@ -535,7 +491,7 @@ class CourseRunSearchSerializerTests(TestCase):
'org': course_run_key.org,
'number': course_run_key.course,
'seat_types': course_run.seat_types,
'image_url': course_run.image_url,
'image_url': course_run.card_image_url,
'type': course_run.type,
'level_type': course_run.level_type.name,
'availability': course_run.availability,
......
......@@ -103,7 +103,7 @@ class AffiliateWindowViewSetTests(ElasticsearchTestMixin, SerializationMixin, AP
self.assertEqual(content.find('name').text, self.course_run.title)
self.assertEqual(content.find('desc').text, self.course_run.short_description)
self.assertEqual(content.find('purl').text, self.course_run.marketing_url)
self.assertEqual(content.find('imgurl').text, self.course_run.image.src)
self.assertEqual(content.find('imgurl').text, self.course_run.card_image_url)
self.assertEqual(content.find('price/actualp').text, str(seat.price))
self.assertEqual(content.find('currency').text, seat.currency.code)
self.assertEqual(content.find('category').text, AffiliateWindowSerializer.CATEGORY)
......
......@@ -168,7 +168,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
response = self.client.get(url)
course_run = self.serialize_catalog_flat_course_run(self.course_run)
course_run_csv = ','.join([
expected = ','.join([
course_run['key'],
course_run['title'],
course_run['pacing_type'],
......@@ -181,9 +181,9 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
course_run['short_description'],
course_run['marketing_url'],
course_run['image']['src'],
course_run['image']['description'],
str(course_run['image']['height']),
str(course_run['image']['width']),
'',
'',
'',
course_run['video']['src'],
course_run['video']['description'],
course_run['video']['image']['src'],
......@@ -219,7 +219,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
])
self.assertEqual(response.status_code, 200)
self.assertIn(course_run_csv, response.content.decode('utf-8'))
self.assertIn(expected, response.content.decode('utf-8'))
def test_get(self):
""" Verify the endpoint returns the details for a single catalog. """
......
......@@ -84,3 +84,7 @@ class Partner(TimeStampedModel):
class Meta:
verbose_name = _('Partner')
verbose_name_plural = _('Partners')
@property
def has_marketing_site(self):
return bool(self.marketing_site_url_root)
""" Tests for core models. """
import ddt
from django.test import TestCase
from social.apps.django_app.default.models import UserSocialAuth
......@@ -56,6 +56,7 @@ class CurrencyTests(TestCase):
self.assertEqual(str(instance), '{code} - {name}'.format(code=code, name=name))
@ddt.ddt
class PartnerTests(TestCase):
""" Tests for the Partner class. """
......@@ -64,3 +65,13 @@ class PartnerTests(TestCase):
partner = PartnerFactory()
self.assertEqual(str(partner), partner.name)
@ddt.unpack
@ddt.data(
('', False),
(None, False),
('https://example.com', True),
)
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
......@@ -4,11 +4,6 @@ from simple_history.admin import SimpleHistoryAdmin
from course_discovery.apps.course_metadata.models import * # pylint: disable=wildcard-import
class CourseOrganizationInline(admin.TabularInline):
model = CourseOrganization
extra = 1
class SeatInline(admin.TabularInline):
model = Seat
extra = 1
......@@ -21,19 +16,21 @@ class PositionInline(admin.TabularInline):
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
inlines = (CourseOrganizationInline,)
list_display = ('key', 'title',)
list_display = ('uuid', 'key', 'title',)
list_filter = ('partner',)
ordering = ('key', 'title',)
search_fields = ('key', 'title',)
readonly_fields = ('uuid',)
search_fields = ('uuid', 'key', 'title',)
@admin.register(CourseRun)
class CourseRunAdmin(admin.ModelAdmin):
inlines = (SeatInline,)
list_display = ('key', 'title',)
list_display = ('uuid', 'key', 'title',)
list_filter = ('course__partner',)
ordering = ('key',)
search_fields = ('key', 'title_override', 'course__title',)
readonly_fields = ('uuid',)
search_fields = ('uuid', 'key', 'title_override', 'course__title',)
@admin.register(Program)
......
......@@ -7,7 +7,7 @@ from edx_rest_api_client.client import EdxRestApiClient
from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.core.utils import delete_orphans
from course_discovery.apps.course_metadata.models import Image, Person, Video
from course_discovery.apps.course_metadata.models import Image, Video
class AbstractDataLoader(metaclass=abc.ABCMeta):
......@@ -104,18 +104,17 @@ class AbstractDataLoader(metaclass=abc.ABCMeta):
return None
@classmethod
def convert_course_run_key(cls, course_run_key_str):
def get_course_key_from_course_run_key(cls, course_run_key):
"""
Given a serialized course run key, return the corresponding
serialized course key.
Args:
course_run_key_str (str): The serialized course run key.
course_run_key (CourseKey): Course run key.
Returns:
str
"""
course_run_key = CourseKey.from_string(course_run_key_str)
return '{org}+{course}'.format(org=course_run_key.org, course=course_run_key.course)
@classmethod
......@@ -125,10 +124,25 @@ class AbstractDataLoader(metaclass=abc.ABCMeta):
delete_orphans(model)
@classmethod
def get_or_create_video(cls, url):
video = None
def _get_or_create_media(cls, media_type, url):
media = None
if url:
video, __ = Video.objects.get_or_create(src=url)
media, __ = media_type.objects.get_or_create(src=url)
return media
@classmethod
def get_or_create_video(cls, url, image_url=None):
video = cls._get_or_create_media(Video, url)
if video:
image = cls.get_or_create_image(image_url)
video.image = image
video.save()
return video
@classmethod
def get_or_create_image(cls, url):
return cls._get_or_create_media(Image, url)
import logging
from decimal import Decimal
from io import BytesIO
import requests
from opaque_keys.edx.keys import CourseKey
import requests
from django.core.files import File
from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.core.models import Currency
from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoader
from course_discovery.apps.course_metadata.models import (
Image, Video, Organization, Seat, CourseRun, Program, Course, CourseOrganization, ProgramType,
Video, Organization, Seat, CourseRun, Program, Course, ProgramType,
)
logger = logging.getLogger(__name__)
......@@ -44,14 +44,16 @@ class OrganizationsApiDataLoader(AbstractDataLoader):
self.delete_orphans()
def update_organization(self, body):
key = body['short_name']
defaults = {
'key': key,
'name': body['name'],
'description': body['description'],
'logo_image_url': body['logo'],
'partner': self.partner,
}
Organization.objects.update_or_create(key=body['short_name'], defaults=defaults)
logger.info('Processed organization "%s"', body['short_name'])
Organization.objects.update_or_create(key__iexact=key, defaults=defaults)
logger.info('Processed organization "%s"', key)
class CoursesApiDataLoader(AbstractDataLoader):
......@@ -94,52 +96,51 @@ class CoursesApiDataLoader(AbstractDataLoader):
self.delete_orphans()
def update_course(self, body):
# NOTE (CCB): Use the data from the CourseKey since the Course API exposes display names for org and number,
# which may not be unique for an organization.
course_run_key_str = body['id']
course_run_key = CourseKey.from_string(course_run_key_str)
organization, __ = Organization.objects.get_or_create(key=course_run_key.org,
defaults={'partner': self.partner})
course_key = self.convert_course_run_key(course_run_key_str)
course_run_key = CourseKey.from_string(body['id'])
course_key = self.get_course_key_from_course_run_key(course_run_key)
defaults = {
'key': course_key,
'title': body['name'],
'partner': self.partner,
}
course, __ = Course.objects.update_or_create(key=course_key, defaults=defaults)
course.organizations.clear()
CourseOrganization.objects.create(
course=course, organization=organization, relation_type=CourseOrganization.OWNER)
course, created = Course.objects.get_or_create(key__iexact=course_key, partner=self.partner, defaults=defaults)
if created:
# NOTE (CCB): Use the data from the CourseKey since the Course API exposes display names for org and number,
# which may not be unique for an organization.
key = course_run_key.org
defaults = {'key': key}
organization, __ = Organization.objects.get_or_create(key__iexact=key, partner=self.partner,
defaults=defaults)
course.authoring_organizations.add(organization)
logger.info('Processed course with key [%s].', course_key)
return course
def update_course_run(self, course, body):
key = body['id']
defaults = {
'course': course,
'key': key,
'start': self.parse_date(body['start']),
'end': self.parse_date(body['end']),
'enrollment_start': self.parse_date(body['enrollment_start']),
'enrollment_end': self.parse_date(body['enrollment_end']),
'title': body['name'],
'short_description': body['short_description'],
'video': self.get_courserun_video(body),
'pacing_type': self.get_pacing_type(body),
}
# If there is no marketing site setup for this partner, use the image from the course API.
# If there is a marketing site defined, it takes prededence.
if not self.partner.marketing_site_url_root:
defaults.update({'image': self.get_courserun_image(body)})
CourseRun.objects.update_or_create(key=body['id'], defaults=defaults)
def get_courserun_image(self, body):
image = None
image_url = body['media'].get('image', {}).get('raw')
# When using a marketing site, only date and pacing information should come from the Course API
if not self.partner.has_marketing_site:
defaults.update({
'card_image_url': body['media'].get('image', {}).get('raw'),
'title_override': body['name'],
'short_description_override': body['short_description'],
'video': self.get_courserun_video(body),
})
if image_url:
image, __ = Image.objects.get_or_create(src=image_url)
course_run, __ = course.course_runs.update_or_create(key__iexact=key, defaults=defaults)
return image
logger.info('Processed course run with key [%s].', course_run.key)
return course_run
def get_pacing_type(self, body):
pacing = body.get('pacing')
......@@ -196,7 +197,7 @@ class EcommerceApiDataLoader(AbstractDataLoader):
def update_seats(self, body):
course_run_key = body['id']
try:
course_run = CourseRun.objects.get(key=course_run_key)
course_run = CourseRun.objects.get(key__iexact=course_run_key)
except CourseRun.DoesNotExist:
logger.warning('Could not find course run [%s]', course_run_key)
return None
......
......@@ -125,7 +125,7 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
)
return bodies
def assert_course_run_loaded(self, body, use_marketing_url=True):
def assert_course_run_loaded(self, body, partner_has_marketing_site=True):
""" Assert a CourseRun corresponding to the specified data body was properly loaded into the database. """
# Validate the Course
......@@ -134,31 +134,43 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
course = Course.objects.get(key=course_key)
self.assertEqual(course.title, body['name'])
self.assertListEqual(list(course.organizations.all()), [organization])
self.assertListEqual(list(course.authoring_organizations.all()), [organization])
# Validate the course run
course_run = CourseRun.objects.get(key=body['id'])
self.assertEqual(course_run.course, course)
self.assertEqual(course_run.title, AbstractDataLoader.clean_string(body['name']))
self.assertEqual(course_run.short_description, AbstractDataLoader.clean_string(body['short_description']))
self.assertEqual(course_run.start, AbstractDataLoader.parse_date(body['start']))
self.assertEqual(course_run.end, AbstractDataLoader.parse_date(body['end']))
self.assertEqual(course_run.enrollment_start, AbstractDataLoader.parse_date(body['enrollment_start']))
self.assertEqual(course_run.enrollment_end, AbstractDataLoader.parse_date(body['enrollment_end']))
self.assertEqual(course_run.pacing_type, self.loader.get_pacing_type(body))
self.assertEqual(course_run.video, self.loader.get_courserun_video(body))
if use_marketing_url:
self.assertEqual(course_run.image, None)
else:
self.assertEqual(course_run.image, self.loader.get_courserun_image(body))
course_run = course.course_runs.get(key=body['id'])
expected_values = {
'title': self.loader.clean_string(body['name']),
'short_description': self.loader.clean_string(body['short_description']),
'start': self.loader.parse_date(body['start']),
'end': self.loader.parse_date(body['end']),
'enrollment_start': self.loader.parse_date(body['enrollment_start']),
'enrollment_end': self.loader.parse_date(body['enrollment_end']),
'pacing_type': self.loader.get_pacing_type(body),
'card_image_url': None,
'title_override': None,
'short_description_override': None,
'video': None,
}
if not partner_has_marketing_site:
expected_values.update({
'card_image_url': body['media'].get('image', {}).get('raw'),
'title_override': body['name'],
'short_description_override': self.loader.clean_string(body['short_description']),
'video': self.loader.get_courserun_video(body),
})
for field, value in expected_values.items():
self.assertEqual(getattr(course_run, field), value, 'Field {} is invalid.'.format(field))
@responses.activate
@ddt.data(True, False)
def test_ingest(self, use_marketing_url):
def test_ingest(self, partner_has_marketing_site):
""" Verify the method ingests data from the Courses API. """
api_data = self.mock_api()
if not use_marketing_url:
if not partner_has_marketing_site:
self.partner.marketing_site_url_root = None
self.partner.save() # pylint: disable=no-member
self.assertEqual(Course.objects.count(), 0)
self.assertEqual(CourseRun.objects.count(), 0)
......@@ -173,7 +185,7 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
self.assertEqual(CourseRun.objects.count(), expected_num_course_runs)
for datum in api_data:
self.assert_course_run_loaded(datum, use_marketing_url)
self.assert_course_run_loaded(datum, partner_has_marketing_site)
# Verify multiple calls to ingest data do NOT result in data integrity errors.
self.loader.ingest()
......
......@@ -5,11 +5,11 @@ from edx_rest_api_client.client import EdxRestApiClient
from course_discovery.apps.core.models import Partner
from course_discovery.apps.course_metadata.data_loaders.api import (
CoursesApiDataLoader, OrganizationsApiDataLoader, EcommerceApiDataLoader, ProgramsApiDataLoader,
OrganizationsApiDataLoader, EcommerceApiDataLoader, ProgramsApiDataLoader, CoursesApiDataLoader,
)
from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
DrupalApiDataLoader, XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader,
XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader, CourseMarketingSiteDataLoader,
)
logger = logging.getLogger(__name__)
......@@ -83,11 +83,11 @@ class Command(BaseCommand):
(partner.marketing_site_url_root, SchoolMarketingSiteDataLoader,),
(partner.marketing_site_url_root, SponsorMarketingSiteDataLoader,),
(partner.marketing_site_url_root, PersonMarketingSiteDataLoader,),
(partner.marketing_site_api_url, CourseMarketingSiteDataLoader,),
(partner.organizations_api_url, OrganizationsApiDataLoader,),
(partner.courses_api_url, CoursesApiDataLoader,),
(partner.ecommerce_api_url, EcommerceApiDataLoader,),
(partner.programs_api_url, ProgramsApiDataLoader,),
(partner.marketing_site_api_url, DrupalApiDataLoader,),
(partner.marketing_site_url_root, XSeriesMarketingSiteDataLoader,),
)
......@@ -97,3 +97,5 @@ class Command(BaseCommand):
loader_class(partner, api_url, access_token, token_type).ingest()
except Exception: # pylint: disable=broad-except
logger.exception('%s failed!', loader_class.__name__)
# TODO Cleanup CourseRun overrides equivalent to the Course values.
......@@ -8,13 +8,13 @@ from django.test import TestCase
from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.core.tests.utils import mock_api_callback
from course_discovery.apps.course_metadata.data_loaders.api import (
CoursesApiDataLoader, OrganizationsApiDataLoader, EcommerceApiDataLoader, ProgramsApiDataLoader,
OrganizationsApiDataLoader, CoursesApiDataLoader, EcommerceApiDataLoader, ProgramsApiDataLoader,
)
from course_discovery.apps.course_metadata.data_loaders.marketing_site import (
DrupalApiDataLoader, XSeriesMarketingSiteDataLoader,
XSeriesMarketingSiteDataLoader, SubjectMarketingSiteDataLoader, SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader, PersonMarketingSiteDataLoader, CourseMarketingSiteDataLoader
)
from course_discovery.apps.course_metadata.data_loaders.tests import mock_data
from course_discovery.apps.course_metadata.models import Course, CourseRun, Organization, Program
ACCESS_TOKEN = 'secret'
JSON = 'application/json'
......@@ -74,7 +74,6 @@ class RefreshCourseMetadataCommandTests(TestCase):
return bodies
def mock_ecommerce_courses_api(self):
bodies = mock_data.ECOMMERCE_API_BODIES
url = self.partner.ecommerce_api_url + 'courses/'
responses.add_callback(
......@@ -108,49 +107,14 @@ class RefreshCourseMetadataCommandTests(TestCase):
)
return bodies
@responses.activate
def test_refresh_course_metadata(self):
""" Verify the refresh_course_metadata management command creates new objects. """
self.mock_apis()
call_command('refresh_course_metadata')
organizations = Organization.objects.all()
self.assertEqual(organizations.count(), 3)
for organization in organizations:
self.assertEqual(organization.partner.short_code, self.partner.short_code)
courses = Course.objects.all()
self.assertEqual(courses.count(), 2)
for course in courses:
self.assertEqual(course.partner.short_code, self.partner.short_code)
course_runs = CourseRun.objects.all()
self.assertEqual(course_runs.count(), 3)
for course_run in course_runs:
self.assertEqual(course_run.course.partner.short_code, self.partner.short_code)
programs = Program.objects.all()
self.assertEqual(programs.count(), 2)
for program in programs:
self.assertEqual(program.partner.short_code, self.partner.short_code)
# Refresh only a specific partner
command_args = ['--partner_code={0}'.format(self.partner.short_code)]
call_command('refresh_course_metadata', *command_args)
@responses.activate
def test_refresh_course_metadata_with_invalid_partner_code(self):
""" Verify an error is raised if an invalid partner code is passed on the command line. """
self.mock_apis()
with self.assertRaises(CommandError):
command_args = ['--partner_code=invalid']
call_command('refresh_course_metadata', *command_args)
@responses.activate
def test_refresh_course_metadata_with_no_token_type(self):
""" Verify an error is raised if an access token is passed in without a token type. """
self.mock_apis()
with self.assertRaises(CommandError):
command_args = ['--access_token=test-access-token']
call_command('refresh_course_metadata', *command_args)
......@@ -164,7 +128,17 @@ class RefreshCourseMetadataCommandTests(TestCase):
with mock.patch(logger_target) as mock_logger:
call_command('refresh_course_metadata')
loader_classes = (OrganizationsApiDataLoader, CoursesApiDataLoader, EcommerceApiDataLoader,
ProgramsApiDataLoader, DrupalApiDataLoader, XSeriesMarketingSiteDataLoader)
loader_classes = (
SubjectMarketingSiteDataLoader,
SchoolMarketingSiteDataLoader,
SponsorMarketingSiteDataLoader,
PersonMarketingSiteDataLoader,
CourseMarketingSiteDataLoader,
OrganizationsApiDataLoader,
CoursesApiDataLoader,
EcommerceApiDataLoader,
ProgramsApiDataLoader,
XSeriesMarketingSiteDataLoader,
)
expected_calls = [mock.call('%s failed!', loader_class.__name__) for loader_class in loader_classes]
mock_logger.exception.assert_has_calls(expected_calls)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import uuid
import django_extensions.db.fields
import sortedm2m.fields
from django.db import migrations, models
def delete_partnerless_courses(apps, schema_editor):
Course = apps.get_model('course_metadata', 'Course')
Course.objects.filter(partner__isnull=True).delete()
def add_uuid_to_courses_and_course_runs(apps, schema_editor):
Course = apps.get_model('course_metadata', 'Course')
CourseRun = apps.get_model('course_metadata', 'CourseRun')
for objects in (Course.objects.filter(uuid__isnull=True), CourseRun.objects.filter(uuid__isnull=True)):
for obj in objects:
obj.uuid = uuid.uuid4()
obj.save()
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0020_auto_20160819_1942'),
]
operations = [
migrations.RunPython(delete_partnerless_courses, reverse_code=migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='courseorganization',
unique_together=set([]),
),
migrations.AlterIndexTogether(
name='courseorganization',
index_together=set([]),
),
migrations.RemoveField(
model_name='courseorganization',
name='course',
),
migrations.RemoveField(
model_name='courseorganization',
name='organization',
),
migrations.AlterModelOptions(
name='course',
options={},
),
migrations.RemoveField(
model_name='courserun',
name='image',
),
migrations.RemoveField(
model_name='courserun',
name='instructors',
),
migrations.RemoveField(
model_name='courserun',
name='marketing_url',
),
migrations.RemoveField(
model_name='historicalcourse',
name='image',
),
migrations.RemoveField(
model_name='historicalcourse',
name='learner_testimonial',
),
migrations.RemoveField(
model_name='historicalcourse',
name='marketing_url',
),
migrations.RemoveField(
model_name='historicalcourserun',
name='image',
),
migrations.RemoveField(
model_name='historicalcourserun',
name='marketing_url',
),
migrations.AddField(
model_name='course',
name='authoring_organizations',
field=sortedm2m.fields.SortedManyToManyField(help_text=None, blank=True, to='course_metadata.Organization',
related_name='authored_courses'),
),
migrations.AddField(
model_name='course',
name='card_image_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='course',
name='slug',
field=django_extensions.db.fields.AutoSlugField(blank=True, populate_from='key', editable=False),
),
migrations.AddField(
model_name='course',
name='sponsoring_organizations',
field=sortedm2m.fields.SortedManyToManyField(help_text=None, blank=True, to='course_metadata.Organization',
related_name='sponsored_courses'),
),
migrations.AddField(
model_name='course',
name='uuid',
field=models.UUIDField(editable=False, verbose_name='UUID', null=True),
),
migrations.AddField(
model_name='courserun',
name='card_image_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='courserun',
name='slug',
field=django_extensions.db.fields.AutoSlugField(blank=True, populate_from='key', editable=False),
),
migrations.AddField(
model_name='courserun',
name='uuid',
field=models.UUIDField(editable=False, verbose_name='UUID', null=True),
),
migrations.AddField(
model_name='historicalcourse',
name='card_image_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='historicalcourse',
name='slug',
field=django_extensions.db.fields.AutoSlugField(blank=True, populate_from='key', editable=False),
),
migrations.AddField(
model_name='historicalcourse',
name='uuid',
field=models.UUIDField(editable=False, verbose_name='UUID', null=True),
),
migrations.AddField(
model_name='historicalcourserun',
name='card_image_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='historicalcourserun',
name='slug',
field=django_extensions.db.fields.AutoSlugField(blank=True, populate_from='key', editable=False),
),
migrations.AddField(
model_name='historicalcourserun',
name='uuid',
field=models.UUIDField(editable=False, verbose_name='UUID', null=True),
),
migrations.AlterField(
model_name='course',
name='key',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='course',
name='partner',
field=models.ForeignKey(to='core.Partner'),
),
migrations.AlterField(
model_name='historicalcourse',
name='key',
field=models.CharField(max_length=255),
),
migrations.RunPython(add_uuid_to_courses_and_course_runs, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='course',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='UUID', editable=False),
),
migrations.AlterField(
model_name='courserun',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='UUID', editable=False),
),
migrations.AlterField(
model_name='historicalcourse',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='UUID', editable=False),
),
migrations.AlterField(
model_name='historicalcourserun',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='UUID', editable=False),
),
migrations.AlterUniqueTogether(
name='course',
unique_together=set([('partner', 'key'), ('partner', 'uuid')]),
),
migrations.RemoveField(
model_name='course',
name='image',
),
migrations.RemoveField(
model_name='course',
name='learner_testimonial',
),
migrations.RemoveField(
model_name='course',
name='marketing_url',
),
migrations.RemoveField(
model_name='course',
name='organizations',
),
migrations.DeleteModel(
name='CourseOrganization',
),
]
......@@ -19,9 +19,9 @@ from taggit.managers import TaggableManager
from course_discovery.apps.core.models import Currency, Partner
from course_discovery.apps.course_metadata.query import CourseQuerySet
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath
from course_discovery.apps.course_metadata.utils import clean_query
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath
logger = logging.getLogger(__name__)
......@@ -219,41 +219,47 @@ class Position(TimeStampedModel):
class Course(TimeStampedModel):
""" Course model. """
key = models.CharField(max_length=255, db_index=True, unique=True)
partner = models.ForeignKey(Partner)
uuid = models.UUIDField(default=uuid4, editable=False, verbose_name=_('UUID'))
key = models.CharField(max_length=255)
title = models.CharField(max_length=255, default=None, null=True, blank=True)
short_description = models.CharField(max_length=255, default=None, null=True, blank=True)
full_description = models.TextField(default=None, null=True, blank=True)
organizations = models.ManyToManyField('Organization', through='CourseOrganization', blank=True)
authoring_organizations = SortedManyToManyField(Organization, blank=True, related_name='authored_courses')
sponsoring_organizations = SortedManyToManyField(Organization, blank=True, related_name='sponsored_courses')
subjects = models.ManyToManyField(Subject, blank=True)
prerequisites = models.ManyToManyField(Prerequisite, blank=True)
level_type = models.ForeignKey(LevelType, default=None, null=True, blank=True)
expected_learning_items = SortedManyToManyField(ExpectedLearningItem, blank=True)
image = models.ForeignKey(Image, default=None, null=True, blank=True)
card_image_url = models.URLField(null=True, blank=True)
slug = AutoSlugField(populate_from='key', editable=True)
video = models.ForeignKey(Video, default=None, null=True, blank=True)
marketing_url = models.URLField(max_length=255, null=True, blank=True)
learner_testimonial = models.CharField(
max_length=50, null=True, blank=True, help_text=_(
"A quote from a learner in the course, demonstrating the value of taking the course"
)
)
number = models.CharField(
max_length=50, null=True, blank=True, help_text=_(
"Course number format e.g CS002x, BIO1.1x, BIO1.2x"
'Course number format e.g CS002x, BIO1.1x, BIO1.2x'
)
)
partner = models.ForeignKey(Partner, null=True, blank=False)
history = HistoricalRecords()
objects = CourseQuerySet.as_manager()
@property
def owners(self):
return self.organizations.filter(courseorganization__relation_type=CourseOrganization.OWNER)
class Meta:
unique_together = (
('partner', 'uuid'),
('partner', 'key'),
)
def __str__(self):
return '{key}: {title}'.format(key=self.key, title=self.title)
@property
def sponsors(self):
return self.organizations.filter(courseorganization__relation_type=CourseOrganization.SPONSOR)
def marketing_url(self):
url = None
if self.partner.marketing_site_url_root:
path = 'course/{slug}'.format(slug=self.slug)
url = urljoin(self.partner.marketing_site_url_root, path)
return url
@property
def active_course_runs(self):
......@@ -289,9 +295,6 @@ class Course(TimeStampedModel):
ids = [result.pk for result in results]
return cls.objects.filter(pk__in=ids)
def __str__(self):
return '{key}: {title}'.format(key=self.key, title=self.title)
class CourseRun(TimeStampedModel):
""" CourseRun model. """
......@@ -307,6 +310,7 @@ class CourseRun(TimeStampedModel):
(INSTRUCTOR_PACED, _('Instructor-paced')),
)
uuid = models.UUIDField(default=uuid4, editable=False, verbose_name=_('UUID'))
course = models.ForeignKey(Course, related_name='course_runs')
key = models.CharField(max_length=255, unique=True)
title_override = models.CharField(
......@@ -328,7 +332,6 @@ class CourseRun(TimeStampedModel):
help_text=_(
"Full description specific for this run of a course. Leave this value blank to default to "
"the parent course's full_description attribute."))
instructors = SortedManyToManyField(Person, blank=True, related_name='courses_instructed')
staff = SortedManyToManyField(Person, blank=True, related_name='courses_staffed')
min_effort = models.PositiveSmallIntegerField(
null=True, blank=True,
......@@ -340,13 +343,18 @@ class CourseRun(TimeStampedModel):
transcript_languages = models.ManyToManyField(LanguageTag, blank=True, related_name='transcript_courses')
pacing_type = models.CharField(max_length=255, choices=PACING_CHOICES, db_index=True, null=True, blank=True)
syllabus = models.ForeignKey(SyllabusItem, default=None, null=True, blank=True)
image = models.ForeignKey(Image, default=None, null=True, blank=True)
card_image_url = models.URLField(null=True, blank=True)
video = models.ForeignKey(Video, default=None, null=True, blank=True)
marketing_url = models.URLField(max_length=255, null=True, blank=True)
slug = AutoSlugField(populate_from='key', editable=True)
history = HistoricalRecords()
@property
def marketing_url(self):
path = 'course/{slug}'.format(slug=self.slug)
return urljoin(self.course.partner.marketing_site_url_root, path)
@property
def title(self):
return self.title_override or self.course.title
......@@ -381,8 +389,12 @@ class CourseRun(TimeStampedModel):
return self.course.subjects
@property
def organizations(self):
return self.course.organizations
def authoring_organizations(self):
return self.course.authoring_organizations
@property
def sponsoring_organizations(self):
return self.course.sponsoring_organizations
@property
def prerequisites(self):
......@@ -390,7 +402,7 @@ class CourseRun(TimeStampedModel):
@property
def programs(self):
return self.course.programs
return self.course.programs # pylint: disable=no-member
@property
def seat_types(self):
......@@ -415,13 +427,6 @@ class CourseRun(TimeStampedModel):
return None
@property
def image_url(self):
if self.image:
return self.image.src
return None
@property
def level_type(self):
return self.course.level_type
......@@ -503,29 +508,6 @@ class Seat(TimeStampedModel):
)
class CourseOrganization(TimeStampedModel):
""" CourseOrganization model. """
OWNER = 'owner'
SPONSOR = 'sponsor'
RELATION_TYPE_CHOICES = (
(OWNER, _('Owner')),
(SPONSOR, _('Sponsor')),
)
course = models.ForeignKey(Course)
organization = models.ForeignKey(Organization)
relation_type = models.CharField(max_length=63, choices=RELATION_TYPE_CHOICES)
class Meta(object):
index_together = (
('course', 'relation_type'),
)
unique_together = (
('course', 'organization', 'relation_type'),
)
class Endorsement(TimeStampedModel):
endorser = models.ForeignKey(Person, blank=False, null=False)
quote = models.TextField(blank=False, null=False)
......@@ -671,10 +653,10 @@ class Program(TimeStampedModel):
return min([course_run.start for course_run in self.course_runs])
@property
def instructors(self):
instructors = [list(course_run.instructors.all()) for course_run in self.course_runs]
instructors = itertools.chain.from_iterable(instructors)
return set(instructors)
def staff(self):
staff = [list(course_run.staff.all()) for course_run in self.course_runs]
staff = itertools.chain.from_iterable(staff)
return set(staff)
class PersonSocialNetwork(AbstractSocialNetworkModel):
......
......@@ -17,8 +17,11 @@ class OrganizationsMixin:
return json.dumps(OrganizationSerializer(organization).data)
def prepare_organizations(self, obj):
return [self.format_organization(organization) for organization in obj.organizations.all()]
def _prepare_organizations(self, organizations):
return [self.format_organization(organization) for organization in organizations]
def prepare_authoring_organizations(self, obj):
return self._prepare_organizations(obj.authoring_organizations.all())
class BaseIndex(indexes.SearchIndex):
......@@ -47,12 +50,23 @@ class BaseCourseIndex(OrganizationsMixin, BaseIndex):
full_description = indexes.CharField(model_attr='full_description', null=True)
subjects = indexes.MultiValueField(faceted=True)
organizations = indexes.MultiValueField(faceted=True)
authoring_organizations = indexes.MultiValueField(faceted=True)
sponsoring_organizations = indexes.MultiValueField(faceted=True)
level_type = indexes.CharField(model_attr='level_type__name', null=True, faceted=True)
partner = indexes.CharField(model_attr='partner__short_code', null=True, faceted=True)
def prepare_subjects(self, obj):
return [subject.name for subject in obj.subjects.all()]
def prepare_organizations(self, obj):
return self.prepare_authoring_organizations(obj) + self.prepare_sponsoring_organizations(obj)
def prepare_authoring_organizations(self, obj):
return self._prepare_organizations(obj.authoring_organizations.all())
def prepare_sponsoring_organizations(self, obj):
return self._prepare_organizations(obj.sponsoring_organizations.all())
class CourseIndex(BaseCourseIndex, indexes.Indexable):
model = Course
......@@ -88,10 +102,11 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
language = indexes.CharField(null=True, faceted=True)
transcript_languages = indexes.MultiValueField(faceted=True)
pacing_type = indexes.CharField(model_attr='pacing_type', null=True, faceted=True)
marketing_url = indexes.CharField(model_attr='marketing_url', null=True)
marketing_url = indexes.CharField(null=True)
slug = indexes.CharField(model_attr='slug', null=True)
seat_types = indexes.MultiValueField(model_attr='seat_types', null=True, faceted=True)
type = indexes.CharField(model_attr='type', null=True, faceted=True)
image_url = indexes.CharField(model_attr='image_url', null=True)
image_url = indexes.CharField(model_attr='card_image_url', null=True)
partner = indexes.CharField(model_attr='course__partner__short_code', null=True, faceted=True)
def _prepare_language(self, language):
......@@ -113,6 +128,9 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
def prepare_transcript_languages(self, obj):
return [self._prepare_language(language) for language in obj.transcript_languages.all()]
def prepare_marketing_url(self, obj):
return obj.marketing_url
class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
model = Program
......@@ -133,14 +151,11 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
def prepare_organizations(self, obj):
return self.prepare_authoring_organizations(obj) + self.prepare_credit_backing_organizations(obj)
def prepare_authoring_organizations(self, obj):
return [self.format_organization(organization) for organization in obj.authoring_organizations.all()]
def prepare_authoring_organization_bodies(self, obj):
return [self.format_organization_body(organization) for organization in obj.authoring_organizations.all()]
def prepare_credit_backing_organizations(self, obj):
return [self.format_organization(organization) for organization in obj.credit_backing_organizations.all()]
return self._prepare_organizations(obj.credit_backing_organizations.all())
def prepare_marketing_url(self, obj):
return obj.marketing_url
{{ object.uuid }}
{{ object.key }}
{{ object.title }}
{{ object.short_description|default:'' }}
......@@ -11,7 +12,11 @@
{{ expected_learning_item.value }}
{% endfor %}
{% for organization in object.organizations.all %}
{% for organization in object.authoring_organizations.all %}
{% include 'search/indexes/course_metadata/partials/organization.txt' %}
{% endfor %}
{% for organization in object.sponsoring_organizations.all %}
{% include 'search/indexes/course_metadata/partials/organization.txt' %}
{% endfor %}
......@@ -22,3 +27,7 @@
{% for subject in object.subjects.all %}
{{ subject.name }}
{% endfor %}
{% for program in object.programs.all %}
{{ program.title }}
{% endfor %}
{% include 'search/indexes/course_metadata/basecourse_text.txt' %}
{{ object.pacing_type|default:'' }}
{{ object.language|default:'' }}
{% for language in object.transcript_languages.all %}
{{ language }}
{% endfor %}
{% for person in object.staff.all %}
{{ person.full_name }}
{% endfor %}
......@@ -75,12 +75,13 @@ class SeatFactory(factory.DjangoModelFactory):
class CourseFactory(factory.DjangoModelFactory):
uuid = factory.LazyFunction(uuid4)
key = FuzzyText(prefix='course-id/')
title = FuzzyText(prefix="Test çօմɾʂҽ ")
short_description = FuzzyText(prefix="Test çօմɾʂҽ short description")
full_description = FuzzyText(prefix="Test çօմɾʂҽ FULL description")
level_type = factory.SubFactory(LevelTypeFactory)
image = factory.SubFactory(ImageFactory)
card_image_url = FuzzyURL()
video = factory.SubFactory(VideoFactory)
marketing_url = FuzzyText(prefix='https://example.com/test-course-url')
partner = factory.SubFactory(PartnerFactory)
......@@ -93,8 +94,19 @@ class CourseFactory(factory.DjangoModelFactory):
if create: # pragma: no cover
add_m2m_data(self.subjects, extracted)
@factory.post_generation
def authoring_organizations(self, create, extracted, **kwargs):
if create:
add_m2m_data(self.authoring_organizations, extracted)
@factory.post_generation
def sponsoring_organizations(self, create, extracted, **kwargs):
if create:
add_m2m_data(self.sponsoring_organizations, extracted)
class CourseRunFactory(factory.DjangoModelFactory):
uuid = factory.LazyFunction(uuid4)
key = FuzzyText(prefix='course-run-id/', suffix='/fake')
course = factory.SubFactory(CourseFactory)
title_override = None
......@@ -106,13 +118,18 @@ class CourseRunFactory(factory.DjangoModelFactory):
enrollment_start = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
enrollment_end = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC)).end_dt
announcement = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
image = factory.SubFactory(ImageFactory)
card_image_url = FuzzyURL()
video = factory.SubFactory(VideoFactory)
min_effort = FuzzyInteger(1, 10)
max_effort = FuzzyInteger(10, 20)
pacing_type = FuzzyChoice([name for name, __ in CourseRun.PACING_CHOICES])
marketing_url = FuzzyText(prefix='https://example.com/test-course-url')
@factory.post_generation
def staff(self, create, extracted, **kwargs):
if create:
add_m2m_data(self.staff, extracted)
class Meta:
model = CourseRun
......
......@@ -13,8 +13,8 @@ from freezegun import freeze_time
from course_discovery.apps.core.models import Currency
from course_discovery.apps.core.utils import SearchQuerySetWrapper
from course_discovery.apps.course_metadata.models import (
AbstractNamedModel, AbstractMediaModel, AbstractValueModel, CourseOrganization, Course, CourseRun,
SeatType)
AbstractNamedModel, AbstractMediaModel, AbstractValueModel, Course, CourseRun, SeatType,
)
from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -29,35 +29,11 @@ class CourseTests(TestCase):
def setUp(self):
super(CourseTests, self).setUp()
self.course = factories.CourseFactory()
self.owner = factories.OrganizationFactory()
self.sponsor = factories.OrganizationFactory()
CourseOrganization.objects.create(
course=self.course,
organization=self.owner,
relation_type=CourseOrganization.OWNER
)
CourseOrganization.objects.create(
course=self.course,
organization=self.sponsor,
relation_type=CourseOrganization.SPONSOR
)
def test_str(self):
""" Verify casting an instance to a string returns a string containing the key and title. """
self.assertEqual(str(self.course), '{key}: {title}'.format(key=self.course.key, title=self.course.title))
def test_owners(self):
""" Verify that the owners property returns only owner related organizations. """
owners = self.course.owners
self.assertEqual(len(owners), 1)
self.assertEqual(owners[0], self.owner)
def test_sponsors(self):
""" Verify that the sponsors property returns only sponsor related organizations. """
sponsors = self.course.sponsors
self.assertEqual(len(sponsors), 1)
self.assertEqual(sponsors[0], self.sponsor)
def test_active_course_runs(self):
""" Verify the property returns only course runs currently open for enrollment or opening in the future. """
self.assertListEqual(list(self.course.active_course_runs), [])
......@@ -143,14 +119,6 @@ class CourseRunTests(TestCase):
expected = sorted([seat.type for seat in seats])
self.assertEqual(sorted(self.course_run.seat_types), expected)
def test_image_url(self):
""" Verify the property returns the associated image's URL. """
self.assertEqual(self.course_run.image_url, self.course_run.image.src)
self.course_run.image = None
self.assertIsNone(self.course_run.image)
self.assertIsNone(self.course_run.image_url)
@ddt.data(
('obviously-wrong', None,),
(('audit',), 'audit',),
......@@ -358,12 +326,12 @@ class ProgramTests(TestCase):
expected_price_ranges = [{'currency': 'USD', 'min': Decimal(100), 'max': Decimal(600)}]
self.assertEqual(program.price_ranges, expected_price_ranges)
def test_instructors(self):
instructors = factories.PersonFactory.create_batch(2)
self.course_runs[0].instructors.add(instructors[0])
self.course_runs[1].instructors.add(instructors[1])
def test_staff(self):
staff = factories.PersonFactory.create_batch(2)
self.course_runs[0].staff.add(staff[0])
self.course_runs[1].staff.add(staff[1])
self.assertEqual(self.program.instructors, set(instructors))
self.assertEqual(self.program.staff, set(staff))
def test_banner_image(self):
self.program.banner_image = make_image_file('test_banner.jpg')
......
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