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