Commit ad501533 by Clinton Blackburn Committed by GitHub

Updated Program model with MicroMasters fields (#196)

ECOM-5094
parent ae5dcc2c
......@@ -50,8 +50,8 @@ PROGRAM_FACET_FIELD_OPTIONS = {
}
PROGRAM_SEARCH_FIELDS = (
'text', 'uuid', 'title', 'subtitle', 'category', 'marketing_url', 'organizations', 'content_type', 'image_url',
'status',
'text', 'uuid', 'title', 'subtitle', 'category', 'marketing_url', 'organizations', 'content_type', 'status',
'card_image_url',
)
......@@ -264,7 +264,7 @@ class ContainedCoursesSerializer(serializers.Serializer):
class ProgramSerializer(serializers.ModelSerializer):
class Meta:
model = Program
fields = ('uuid', 'title', 'subtitle', 'category', 'marketing_slug', 'marketing_url', 'image_url',)
fields = ('uuid', 'title', 'subtitle', 'category', 'marketing_slug', 'marketing_url', 'card_image_url',)
read_only_fields = ('uuid', 'marketing_url',)
......
......@@ -181,7 +181,7 @@ class ProgramSerializerTests(TestCase):
'category': program.category,
'marketing_slug': program.marketing_slug,
'marketing_url': program.marketing_url,
'image_url': program.image_url,
'card_image_url': program.card_image_url,
}
self.assertDictEqual(serializer.data, expected)
......@@ -386,9 +386,12 @@ class CourseRunSearchSerializerTests(TestCase):
class ProgramSearchSerializerTests(TestCase):
def test_data(self):
program = ProgramFactory()
organization = OrganizationFactory()
program.organizations.add(organization)
authoring_organization, crediting_organization = OrganizationFactory.create_batch(2)
program.authoring_organizations.add(authoring_organization)
program.credit_backing_organizations.add(crediting_organization)
program.save()
expected_organizations = [OrganizationsMixin.format_organization(org) for org in
(authoring_organization, crediting_organization)]
# NOTE: This serializer expects SearchQuerySet results, so we run a search on the newly-created object
# to generate such a result.
......@@ -401,9 +404,9 @@ class ProgramSearchSerializerTests(TestCase):
'subtitle': program.subtitle,
'category': program.category,
'marketing_url': program.marketing_url,
'organizations': [OrganizationsMixin.format_organization(organization)],
'organizations': expected_organizations,
'content_type': 'program',
'image_url': program.image_url,
'card_image_url': program.card_image_url,
'status': program.status,
}
self.assertDictEqual(serializer.data, expected)
from django.contrib import admin
from course_discovery.apps.course_metadata.models import (
Seat, Image, Video, LevelType, Subject, Prerequisite, ExpectedLearningItem, Expertise,
Course, CourseRun, CourseRunSocialNetwork, MajorWork, Organization, Person, PersonSocialNetwork,
CourseOrganization, SyllabusItem, Program
)
Seat, Image, Video, LevelType, Subject, Prerequisite, ExpectedLearningItem, Expertise, Course, CourseRun,
CourseRunSocialNetwork, MajorWork, Organization, Person, PersonSocialNetwork, CourseOrganization, SyllabusItem,
Program, JobOutlookItem, SeatType, Endorsement, CorporateEndorsement, FAQ, ProgramType)
class CourseOrganizationInline(admin.TabularInline):
......@@ -21,6 +20,7 @@ class SeatInline(admin.TabularInline):
class CourseAdmin(admin.ModelAdmin):
inlines = (CourseOrganizationInline,)
list_display = ('key', 'title',)
list_filter = ('partner',)
ordering = ('key', 'title',)
search_fields = ('key', 'title',)
......@@ -36,10 +36,38 @@ class CourseRunAdmin(admin.ModelAdmin):
@admin.register(Program)
class ProgramAdmin(admin.ModelAdmin):
list_display = ('uuid', 'title',)
list_filter = ('partner',)
ordering = ('uuid', 'title',)
readonly_fields = ('uuid',)
search_fields = ('uuid', 'title', 'marketing_slug')
@admin.register(ProgramType)
class ProgramTypeAdmin(admin.ModelAdmin):
list_display = ('name',)
@admin.register(SeatType)
class SeatTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'slug',)
readonly_fields = ('slug',)
@admin.register(Endorsement)
class EndorsementAdmin(admin.ModelAdmin):
list_display = ('endorser',)
@admin.register(CorporateEndorsement)
class CorporateEndorsementAdmin(admin.ModelAdmin):
list_display = ('corporation_name',)
@admin.register(FAQ)
class FAQAdmin(admin.ModelAdmin):
list_display = ('question',)
class KeyNameAdmin(admin.ModelAdmin):
list_display = ('key', 'name',)
ordering = ('key', 'name',)
......@@ -61,5 +89,6 @@ for model in (LevelType, Subject, Prerequisite, Expertise, MajorWork):
admin.site.register(model, NamedModelAdmin)
# Register remaining models using basic ModelAdmin classes
for model in (Image, Video, ExpectedLearningItem, SyllabusItem, PersonSocialNetwork, CourseRunSocialNetwork):
for model in (Image, Video, ExpectedLearningItem, SyllabusItem, PersonSocialNetwork, CourseRunSocialNetwork,
JobOutlookItem,):
admin.site.register(model)
......@@ -307,7 +307,8 @@ class DrupalApiDataLoader(AbstractDataLoader):
# Clean Organizations separately from other orphaned instances to avoid removing all orgnaziations
# after an initial data load on an empty table.
Organization.objects.filter(courseorganization__isnull=True, program__isnull=True).delete()
Organization.objects.filter(courseorganization__isnull=True, authored_programs__isnull=True,
credit_backed_programs__isnull=True).delete()
self.delete_orphans()
logger.info('Retrieved %d course runs from %s.', len(data), api_url)
......@@ -545,7 +546,7 @@ class ProgramsApiDataLoader(AbstractDataLoader):
'category': body['category'],
'status': body['status'],
'marketing_slug': body['marketing_slug'],
'image': self._get_image(body),
'banner_image_url': self._get_banner_image_url(body),
'partner': self.partner,
}
......@@ -558,24 +559,15 @@ class ProgramsApiDataLoader(AbstractDataLoader):
)
organizations.append(organization)
program.organizations.clear()
program.organizations.add(*organizations)
program.authoring_organizations.clear()
program.authoring_organizations.add(*organizations)
except Exception: # pylint: disable=broad-except
logger.exception('Failed to load program %s', uuid)
def _get_image(self, body):
image = None
def _get_banner_image_url(self, body):
image_key = 'w{width}h{height}'.format(width=self.image_width, height=self.image_height)
image_url = body.get('banner_image_urls', {}).get(image_key)
if image_url:
defaults = {
'width': self.image_width,
'height': self.image_height,
}
image, __ = Image.objects.update_or_create(src=image_url, defaults=defaults)
return image
return image_url
class MarketingSiteDataLoader(AbstractDataLoader):
......@@ -667,10 +659,7 @@ class MarketingSiteDataLoader(AbstractDataLoader):
'subtitle': data.get('field_xseries_subtitle_short'),
'category': 'XSeries',
'partner': self.partner,
'card_image_url': card_image_url,
}
if card_image_url:
card_image, __ = Image.objects.get_or_create(src=card_image_url)
defaults['image'] = card_image
Program.objects.update_or_create(marketing_slug=marketing_slug, defaults=defaults)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
SEAT_TYPES = ('Audit', 'Credit', 'Professional', 'Verified',)
def add_seat_types(apps, schema_editor):
SeatType = apps.get_model('course_metadata', 'SeatType')
for name in SEAT_TYPES:
SeatType.objects.update_or_create(name=name)
def drop_seat_types(apps, schema_editor):
SeatType = apps.get_model('course_metadata', 'SeatType')
SeatType.objects.filter(name__in=SEAT_TYPES).delete()
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0011_auto_20160805_1949'),
]
operations = [
migrations.RunPython(add_seat_types, drop_seat_types)
]
......@@ -106,7 +106,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
return [self._prepare_language(language) for language in obj.transcript_languages.all()]
class ProgramIndex(OrganizationsMixin, BaseIndex, indexes.Indexable):
class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
model = Program
uuid = indexes.CharField(model_attr='uuid')
......@@ -115,9 +115,20 @@ class ProgramIndex(OrganizationsMixin, BaseIndex, indexes.Indexable):
category = indexes.CharField(model_attr='category', faceted=True)
marketing_url = indexes.CharField(null=True)
organizations = indexes.MultiValueField(faceted=True)
image_url = indexes.CharField(model_attr='image_url', null=True)
authoring_organizations = indexes.MultiValueField(faceted=True)
credit_backing_organizations = indexes.MultiValueField(faceted=True)
card_image_url = indexes.CharField(model_attr='card_image_url', null=True)
status = indexes.CharField(model_attr='status', faceted=True)
partner = indexes.CharField(model_attr='partner__name', null=True, faceted=True)
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_credit_backing_organizations(self, obj):
return [self.format_organization(organization) for organization in obj.credit_backing_organizations.all()]
def prepare_marketing_url(self, obj):
return obj.marketing_url
......@@ -7,16 +7,18 @@ from factory.fuzzy import (
)
from pytz import UTC
from course_discovery.apps.core.models import Currency
from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.core.tests.utils import FuzzyURL
from course_discovery.apps.core.models import Currency
from course_discovery.apps.course_metadata.models import (
Course, CourseRun, Organization, Person, Image, Video, Subject, Seat, Prerequisite, LevelType, Program,
AbstractSocialNetworkModel, CourseRunSocialNetwork, PersonSocialNetwork
AbstractSocialNetworkModel, CourseRunSocialNetwork, PersonSocialNetwork, ProgramType
)
from course_discovery.apps.ietf_language_tags.models import LanguageTag
# pylint: disable=no-member, unused-argument
class AbstractMediaModelFactory(factory.DjangoModelFactory):
src = FuzzyURL()
description = FuzzyText()
......@@ -80,6 +82,16 @@ class CourseFactory(factory.DjangoModelFactory):
class Meta:
model = Course
@factory.post_generation
def subjects(self, create, extracted, **kwargs):
if not create: # pragma: no cover
# Simple build, do nothing.
return
if extracted:
for subject in extracted:
self.subjects.add(subject)
class CourseRunFactory(factory.DjangoModelFactory):
key = FuzzyText(prefix='course-run-id/', suffix='/fake')
......@@ -103,6 +115,16 @@ class CourseRunFactory(factory.DjangoModelFactory):
class Meta:
model = CourseRun
@factory.post_generation
def transcript_languages(self, create, extracted, **kwargs):
if not create: # pragma: no cover
# Simple build, do nothing.
return
if extracted:
for transcript_language in extracted:
self.transcript_languages.add(transcript_language)
class OrganizationFactory(factory.DjangoModelFactory):
key = FuzzyText(prefix='Org.fake/')
......@@ -127,6 +149,23 @@ class PersonFactory(factory.DjangoModelFactory):
model = Person
class ProgramTypeFactory(factory.django.DjangoModelFactory):
class Meta(object):
model = ProgramType
name = FuzzyText()
@factory.post_generation
def applicable_seat_types(self, create, extracted, **kwargs):
if not create: # pragma: no cover
# Simple build, do nothing.
return
if extracted:
for seat_type in extracted:
self.applicable_seat_types.add(seat_type)
class ProgramFactory(factory.django.DjangoModelFactory):
class Meta(object):
model = Program
......@@ -137,9 +176,31 @@ class ProgramFactory(factory.django.DjangoModelFactory):
category = 'xseries'
status = 'unpublished'
marketing_slug = factory.Sequence(lambda n: 'test-slug-{}'.format(n)) # pylint: disable=unnecessary-lambda
image = factory.SubFactory(ImageFactory)
banner_image_url = FuzzyText(prefix='https://example.com/program/banner')
card_image_url = FuzzyText(prefix='https://example.com/program/card')
partner = factory.SubFactory(PartnerFactory)
@factory.post_generation
def courses(self, create, extracted, **kwargs):
if not create: # pragma: no cover
# Simple build, do nothing.
return
if extracted:
# Use the passed in list of courses
for course in extracted:
self.courses.add(course)
@factory.post_generation
def excluded_course_runs(self, create, extracted, **kwargs):
if not create: # pragma: no cover
# Simple build, do nothing.
return
if extracted:
for course_run in extracted:
self.excluded_course_runs.add(course_run)
class AbstractSocialNetworkModelFactory(factory.DjangoModelFactory):
type = FuzzyChoice([name for name, __ in AbstractSocialNetworkModel.SOCIAL_NETWORK_CHOICES])
......
......@@ -636,13 +636,10 @@ class ProgramsApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCa
keys = [org['key'] for org in body['organizations']]
expected_organizations = list(Organization.objects.filter(key__in=keys))
self.assertEqual(keys, [org.key for org in expected_organizations])
self.assertListEqual(list(program.organizations.all()), expected_organizations)
self.assertListEqual(list(program.authoring_organizations.all()), expected_organizations)
image_url = body.get('banner_image_urls', {}).get('w435h145')
if image_url:
image = Image.objects.get(src=image_url, width=self.loader.image_width,
height=self.loader.image_height)
self.assertEqual(program.image, image)
banner_image_url = body.get('banner_image_urls', {}).get('w435h145')
self.assertEqual(program.banner_image_url, banner_image_url)
@responses.activate
def test_ingest(self):
......@@ -736,12 +733,7 @@ class MarketingSiteDataLoaderTests(DataLoaderTestMixin, TestCase):
self.assertEqual(program.partner, self.partner)
card_image_url = data.get('field_card_image', {}).get('url')
if card_image_url:
card_image = Image.objects.get(src=card_image_url)
self.assertEqual(program.image, card_image)
else:
self.assertIsNone(program.image)
self.assertEqual(program.card_image_url, card_image_url)
def test_constructor_without_credentials(self):
""" Verify the constructor raises an exception if the Partner has no marketing site credentials set. """
......
import datetime
import itertools
from decimal import Decimal
import ddt
import mock
import pytz
from dateutil.parser import parse
from django.db import IntegrityError
from django.test import TestCase
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
)
AbstractNamedModel, AbstractMediaModel, AbstractValueModel, CourseOrganization, Course, CourseRun,
SeatType)
from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag
# pylint: disable=no-member
class CourseTests(TestCase):
""" Tests for the `Course` model. """
......@@ -270,7 +274,13 @@ class ProgramTests(TestCase):
def setUp(self):
super(ProgramTests, self).setUp()
self.program = factories.ProgramFactory()
transcript_languages = LanguageTag.objects.all()[:2]
subjects = factories.SubjectFactory.create_batch(2)
self.course_runs = factories.CourseRunFactory.create_batch(
3, transcript_languages=transcript_languages, course__subjects=subjects)
self.courses = [course_run.course for course_run in self.course_runs]
self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0])
self.program = factories.ProgramFactory(courses=self.courses, excluded_course_runs=[self.excluded_course_run])
def test_str(self):
"""Verify that a program is properly converted to a str."""
......@@ -287,13 +297,56 @@ class ProgramTests(TestCase):
self.program.marketing_slug = ''
self.assertIsNone(self.program.marketing_url)
def test_image_url(self):
""" Verify the property returns the associated image's URL. """
self.assertEqual(self.program.image_url, self.program.image.src)
self.program.image = None
self.assertIsNone(self.program.image)
self.assertIsNone(self.program.image_url)
def test_course_runs(self):
""" Verify the property returns the set of associated CourseRuns minus those that are explicitly excluded. """
self.assertEqual(set(self.program.course_runs), set(self.course_runs))
def test_languages(self):
expected_languages = set([course_run.language for course_run in self.course_runs])
actual_languages = self.program.languages
self.assertGreater(len(actual_languages), 0)
self.assertEqual(actual_languages, expected_languages)
def test_transcript_languages(self):
expected_transcript_languages = itertools.chain.from_iterable(
[list(course_run.transcript_languages.all()) for course_run in self.course_runs])
expected_transcript_languages = set(expected_transcript_languages)
actual_transcript_languages = self.program.transcript_languages
self.assertGreater(len(actual_transcript_languages), 0)
self.assertEqual(actual_transcript_languages, expected_transcript_languages)
def test_subjects(self):
expected_subjects = itertools.chain.from_iterable([list(course.subjects.all()) for course in self.courses])
expected_subjects = set(expected_subjects)
actual_subjects = self.program.subjects
self.assertGreater(len(actual_subjects), 0)
self.assertEqual(actual_subjects, expected_subjects)
def test_start(self):
expected_start = min([course_run.start for course_run in self.course_runs])
self.assertEqual(self.program.start, expected_start)
def test_price_ranges(self):
currency = Currency.objects.get(code='USD')
course_run = factories.CourseRunFactory()
factories.SeatFactory(type='audit', currency=currency, course_run=course_run, price=0)
factories.SeatFactory(type='credit', currency=currency, course_run=course_run, price=600)
factories.SeatFactory(type='verified', currency=currency, course_run=course_run, price=100)
applicable_seat_types = SeatType.objects.filter(slug__in=['credit', 'verified'])
program_type = factories.ProgramTypeFactory(name='XSeries', applicable_seat_types=applicable_seat_types)
program = factories.ProgramFactory(type=program_type, courses=[course_run.course])
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])
self.assertEqual(self.program.instructors, set(instructors))
class PersonSocialNetworkTests(TestCase):
......
cryptography==1.4
django==1.8.14
django-extensions==1.6.7
django-choices==1.4.3
django-compressor==2.0
django-extensions==1.6.7
django-filter==0.13.0
django-guardian==1.4.4
django-haystack==2.4.1
......
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