Commit caf729cc by Matthew Piatetsky

add program types to search endpoint

parent 17013506
...@@ -45,7 +45,7 @@ COURSE_RUN_FACET_FIELD_QUERIES = { ...@@ -45,7 +45,7 @@ COURSE_RUN_FACET_FIELD_QUERIES = {
COURSE_RUN_SEARCH_FIELDS = ( COURSE_RUN_SEARCH_FIELDS = (
'text', 'key', 'title', 'short_description', 'full_description', 'start', 'end', 'enrollment_start', 'text', 'key', 'title', 'short_description', 'full_description', 'start', 'end', 'enrollment_start',
'enrollment_end', 'pacing_type', 'language', 'transcript_languages', 'marketing_url', 'content_type', 'org', 'enrollment_end', 'pacing_type', 'language', 'transcript_languages', 'marketing_url', 'content_type', 'org',
'number', 'seat_types', 'image_url', 'type', 'level_type', 'availability', 'published', 'partner', 'number', 'seat_types', 'image_url', 'type', 'level_type', 'availability', 'published', 'partner', 'program_types',
) )
PROGRAM_FACET_FIELD_OPTIONS = { PROGRAM_FACET_FIELD_OPTIONS = {
......
...@@ -584,6 +584,7 @@ class AffiliateWindowSerializerTests(TestCase): ...@@ -584,6 +584,7 @@ class AffiliateWindowSerializerTests(TestCase):
class CourseRunSearchSerializerTests(TestCase): class CourseRunSearchSerializerTests(TestCase):
def test_data(self): def test_data(self):
course_run = CourseRunFactory(transcript_languages=LanguageTag.objects.filter(code__in=['en-us', 'zh-cn'])) course_run = CourseRunFactory(transcript_languages=LanguageTag.objects.filter(code__in=['en-us', 'zh-cn']))
ProgramFactory(courses=[course_run.course])
serializer = self.serialize_course_run(course_run) serializer = self.serialize_course_run(course_run)
course_run_key = CourseKey.from_string(course_run.key) course_run_key = CourseKey.from_string(course_run.key)
...@@ -610,6 +611,7 @@ class CourseRunSearchSerializerTests(TestCase): ...@@ -610,6 +611,7 @@ class CourseRunSearchSerializerTests(TestCase):
'availability': course_run.availability, 'availability': course_run.availability,
'published': course_run.status == CourseRun.Status.Published, 'published': course_run.status == CourseRun.Status.Published,
'partner': course_run.course.partner.short_code, 'partner': course_run.course.partner.short_code,
'program_types': course_run.program_types,
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
......
...@@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _
from django_extensions.db.fields import AutoSlugField from django_extensions.db.fields import AutoSlugField
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
from djchoices import DjangoChoices, ChoiceItem from djchoices import DjangoChoices, ChoiceItem
from haystack import connections
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
...@@ -307,6 +308,19 @@ class Course(TimeStampedModel): ...@@ -307,6 +308,19 @@ 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 save(self, *args, **kwargs):
super(Course, self).save(*args, **kwargs)
try:
self.reindex_course_runs()
except Exception: # pylint: disable=broad-except
logger.exception("An error occurred while attempting to reindex the course runs"
"of Course with key: [{key}].".format(key=self.key))
def reindex_course_runs(self):
index = connections['default'].get_unified_index().get_index(CourseRun)
for course_run in self.course_runs.all():
index.update_object(course_run)
class CourseRun(TimeStampedModel): class CourseRun(TimeStampedModel):
""" CourseRun model. """ """ CourseRun model. """
...@@ -366,6 +380,10 @@ class CourseRun(TimeStampedModel): ...@@ -366,6 +380,10 @@ class CourseRun(TimeStampedModel):
objects = CourseRunQuerySet.as_manager() objects = CourseRunQuerySet.as_manager()
@property @property
def program_types(self):
return [program.type.name for program in self.programs.all()]
@property
def marketing_url(self): def marketing_url(self):
if self.slug: if self.slug:
path = 'course/{slug}'.format(slug=self.slug) path = 'course/{slug}'.format(slug=self.slug)
...@@ -724,6 +742,17 @@ class Program(TimeStampedModel): ...@@ -724,6 +742,17 @@ class Program(TimeStampedModel):
publisher.publish_program(self) publisher.publish_program(self)
else: else:
super(Program, self).save(*args, **kwargs) super(Program, self).save(*args, **kwargs)
self.reindex_courses()
def reindex_courses(self):
try:
index = connections['default'].get_unified_index().get_index(Course)
for course in self.courses.all():
index.update_object(course)
course.reindex_course_runs()
except Exception: # pylint: disable=broad-except
logger.exception("An error occurred while attempting to reindex the courses"
"of Program with uuid: [{uuid}].".format(uuid=self.uuid))
class PersonSocialNetwork(AbstractSocialNetworkModel): class PersonSocialNetwork(AbstractSocialNetworkModel):
......
...@@ -112,6 +112,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable): ...@@ -112,6 +112,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
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='card_image_url', null=True) image_url = indexes.CharField(model_attr='card_image_url', null=True)
partner = indexes.CharField(null=True, faceted=True) partner = indexes.CharField(null=True, faceted=True)
program_types = indexes.MultiValueField()
published = indexes.BooleanField(null=False, faceted=True) published = indexes.BooleanField(null=False, faceted=True)
def prepare_partner(self, obj): def prepare_partner(self, obj):
...@@ -147,6 +148,9 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable): ...@@ -147,6 +148,9 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
def prepare_marketing_url(self, obj): def prepare_marketing_url(self, obj):
return obj.marketing_url return obj.marketing_url
def prepare_program_types(self, obj):
return obj.program_types
class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin): class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
model = Program model = Program
......
...@@ -2,6 +2,7 @@ import itertools ...@@ -2,6 +2,7 @@ import itertools
from decimal import Decimal from decimal import Decimal
import ddt import ddt
import mock
from dateutil.parser import parse from dateutil.parser import parse
from django.conf import settings from django.conf import settings
from django.db import IntegrityError from django.db import IntegrityError
...@@ -15,8 +16,8 @@ from course_discovery.apps.core.tests.helpers import make_image_file ...@@ -15,8 +16,8 @@ from course_discovery.apps.core.tests.helpers import make_image_file
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 (
AbstractMediaModel, AbstractNamedModel, AbstractValueModel, AbstractMediaModel, AbstractNamedModel, AbstractValueModel,
CorporateEndorsement, Course, CourseRun, Endorsement, CorporateEndorsement, Program, Course, CourseRun, Endorsement,
FAQ, SeatType, Program FAQ, SeatType
) )
from course_discovery.apps.course_metadata.tests import factories, toggle_switch from course_discovery.apps.course_metadata.tests import factories, toggle_switch
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ImageFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ImageFactory
...@@ -47,6 +48,11 @@ class CourseTests(TestCase): ...@@ -47,6 +48,11 @@ class CourseTests(TestCase):
actual = list(Course.search(query).order_by('key')) actual = list(Course.search(query).order_by('key'))
self.assertEqual(actual, courses) self.assertEqual(actual, courses)
def test_course_run_update_caught_exception(self):
""" Test that the index update process failing will not cause the course save to error """
with mock.patch.object(Course, 'reindex_course_runs', side_effect=Exception):
self.course.save()
@ddt.ddt @ddt.ddt
class CourseRunTests(TestCase): class CourseRunTests(TestCase):
...@@ -150,6 +156,13 @@ class CourseRunTests(TestCase): ...@@ -150,6 +156,13 @@ class CourseRunTests(TestCase):
course_run = CourseRunFactory(slug=slug) course_run = CourseRunFactory(slug=slug)
self.assertIsNone(course_run.marketing_url) self.assertIsNone(course_run.marketing_url)
def test_program_types(self):
""" Verify the property retrieves program types correctly based on programs. """
courses = [self.course_run.course]
program = factories.ProgramFactory(courses=courses)
other_program = factories.ProgramFactory(courses=courses)
self.assertCountEqual(self.course_run.program_types, [program.type.name, other_program.type.name])
class OrganizationTests(TestCase): class OrganizationTests(TestCase):
""" Tests for the `Organization` model. """ """ Tests for the `Organization` model. """
...@@ -409,6 +422,11 @@ class ProgramTests(MarketingSitePublisherTestMixin, TestCase): ...@@ -409,6 +422,11 @@ class ProgramTests(MarketingSitePublisherTestMixin, TestCase):
self.assertEqual(len(responses.calls), 0) self.assertEqual(len(responses.calls), 0)
toggle_switch('publish_program_to_marketing_site', False) toggle_switch('publish_program_to_marketing_site', False)
def test_course_update_caught_exception(self):
""" Test that the index update process failing will not cause the program save to error """
with mock.patch.object(Course, 'reindex_course_runs', side_effect=Exception):
self.program.save()
class PersonSocialNetworkTests(TestCase): class PersonSocialNetworkTests(TestCase):
"""Tests of the PersonSocialNetwork model.""" """Tests of the PersonSocialNetwork model."""
......
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