Commit cdf121f6 by Clinton Blackburn Committed by GitHub

Exposing additional course run fields via search API (#142)

ECOM-4747
parent 84401d0d
from rest_framework import pagination
class PageNumberPagination(pagination.PageNumberPagination):
page_size_query_param = 'page_size'
......@@ -29,16 +29,18 @@ COURSE_RUN_FACET_FIELD_OPTIONS = {
'transcript_languages': {},
'pacing_type': {},
'start': {
"start_date": datetime.datetime.now() - datetime.timedelta(days=365),
"end_date": datetime.datetime.now(),
"gap_by": "month",
"gap_amount": 1,
'start_date': datetime.datetime.now() - datetime.timedelta(days=365),
'end_date': datetime.datetime.now(),
'gap_by': 'month',
'gap_amount': 1,
},
'content_type': {},
'type': {},
}
COURSE_RUN_SEARCH_FIELDS = (
'key', 'title', 'short_description', 'full_description', 'start', 'end', 'enrollment_start', 'enrollment_end',
'pacing_type', 'language', 'transcript_languages', 'marketing_url', 'content_type', 'text',
'pacing_type', 'language', 'transcript_languages', 'marketing_url', 'content_type', 'org', 'number', 'seat_types',
'image_url', 'type', 'text',
)
......
......@@ -3,22 +3,27 @@ from urllib.parse import urlencode
import ddt
from django.test import TestCase
from haystack.query import SearchQuerySet
from opaque_keys.edx.keys import CourseKey
from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import(
from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer,
PersonSerializer, AffiliateWindowSerializer, ContainedCourseRunsSerializer
)
PersonSerializer, AffiliateWindowSerializer, ContainedCourseRunsSerializer,
CourseRunSearchSerializer)
from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.course_metadata.models import CourseRun
from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory,
ImageFactory, VideoFactory, OrganizationFactory, PersonFactory, SeatFactory
)
# pylint:disable=no-member
def json_date_format(datetime_obj):
return datetime.strftime(datetime_obj, "%Y-%m-%dT%H:%M:%S.%fZ")
......@@ -314,3 +319,42 @@ class AffiliateWindowSerializerTests(TestCase):
'category': 'Other Experiences'
}
self.assertDictEqual(serializer.data, expected)
class CourseRunSearchSerializerTests(TestCase):
def serialize_datetime(self, d):
return d.strftime('%Y-%m-%dT%H:%M:%S') if d else None
def serialize_language(self, language):
return language.name
def test_data(self):
course_run = CourseRunFactory()
course_run_key = CourseKey.from_string(course_run.key)
# NOTE: This serializer expects SearchQuerySet results, so we run a search on the newly-created object
# to generate such a result.
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
serializer = CourseRunSearchSerializer(result)
expected = {
'transcript_languages': [self.serialize_language(l) for l in course_run.transcript_languages.all()],
'short_description': course_run.short_description,
'start': self.serialize_datetime(course_run.start),
'end': self.serialize_datetime(course_run.end),
'enrollment_start': self.serialize_datetime(course_run.enrollment_start),
'enrollment_end': self.serialize_datetime(course_run.enrollment_end),
'key': course_run.key,
'marketing_url': course_run.marketing_url,
'pacing_type': course_run.pacing_type,
'language': self.serialize_language(course_run.language),
'full_description': course_run.full_description,
'title': course_run.title,
'content_type': 'courserun',
'org': course_run_key.org,
'number': course_run_key.course,
'seat_types': course_run.seat_types,
'image_url': course_run.image_url,
'type': course_run.type,
}
self.assertDictEqual(serializer.data, expected)
......@@ -3,10 +3,13 @@ import urllib.parse
import ddt
from django.core.urlresolvers import reverse
from haystack.query import SearchQuerySet
from rest_framework.test import APITestCase
from course_discovery.apps.api.serializers import CourseRunSearchSerializer
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.models import CourseRun
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory
......@@ -31,28 +34,9 @@ class CourseRunSearchViewSetTests(ElasticsearchTestMixin, APITestCase):
url = '{path}?{qs}'.format(path=path, qs=qs)
return self.client.get(url)
def serialize_date(self, d):
return d.strftime('%Y-%m-%dT%H:%M:%S') if d else None
def serialize_language(self, language):
return language.name
def serialize_course_run(self, course_run):
return {
'transcript_languages': [self.serialize_language(l) for l in course_run.transcript_languages.all()],
'short_description': course_run.short_description,
'start': self.serialize_date(course_run.start),
'end': self.serialize_date(course_run.end),
'enrollment_start': self.serialize_date(course_run.enrollment_start),
'enrollment_end': self.serialize_date(course_run.enrollment_end),
'key': course_run.key,
'marketing_url': course_run.marketing_url,
'pacing_type': course_run.pacing_type,
'language': self.serialize_language(course_run.language),
'full_description': course_run.full_description,
'title': course_run.title,
'content_type': 'courserun'
}
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
return CourseRunSearchSerializer(result).data
@ddt.data(True, False)
def test_authentication(self, faceted):
......
......@@ -19,12 +19,12 @@ from edx_rest_framework_extensions.permissions import IsSuperuser
from rest_framework import status, viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from course_discovery.apps.api import serializers
from course_discovery.apps.api.filters import PermissionsFilter
from course_discovery.apps.api.pagination import PageNumberPagination
from course_discovery.apps.api.renderers import AffiliateWindowXMLRenderer, CourseRunCSVRenderer
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.core.utils import SearchQuerySetWrapper
......
......@@ -275,6 +275,35 @@ class CourseRun(TimeStampedModel):
def organizations(self):
return self.course.organizations
@property
def seat_types(self):
return list(self.seats.values_list('type', flat=True))
@property
def type(self):
seat_types = set(self.seat_types)
mapping = (
('credit', {'credit'}),
('professional', {'professional', 'no-id-professional'}),
('verified', {'verified'}),
('honor', {'honor'}),
('audit', {'audit'}),
)
for course_run_type, matching_seat_types in mapping:
if matching_seat_types & seat_types:
return course_run_type
logger.warning('Unable to determine type for course run [%s]. Seat types are [%s]', self.key, seat_types)
return None
@property
def image_url(self):
if self.image:
return self.image.src
return None
@classmethod
def search(cls, query):
""" Queries the search index.
......
......@@ -75,6 +75,9 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
transcript_languages = indexes.MultiValueField(faceted=True)
pacing_type = indexes.CharField(model_attr='pacing_type', null=True, faceted=True)
marketing_url = indexes.CharField(model_attr='marketing_url', null=True)
seat_types = indexes.MultiValueField(model_attr='seat_types', null=True, faceted=True)
type = indexes.CharField(model_attr='type', null=True, faceted=True)
image_url = indexes.CharField(model_attr='image_url', null=True)
def _prepare_language(self, language):
return language.name
......
import datetime
import ddt
import mock
import pytz
from django.test import TestCase
......@@ -11,6 +12,8 @@ from course_discovery.apps.course_metadata.models import (
from course_discovery.apps.course_metadata.tests import factories
# pylint: disable=no-member
class CourseTests(TestCase):
""" Tests for the `Course` model. """
......@@ -36,19 +39,18 @@ class CourseTests(TestCase):
def test_owners(self):
""" Verify that the owners property returns only owner related organizations. """
owners = self.course.owners # pylint: disable=no-member
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 # pylint: disable=no-member
sponsors = self.course.sponsors
self.assertEqual(len(sponsors), 1)
self.assertEqual(sponsors[0], self.sponsor)
def test_active_course_runs(self):
""" Verify the property returns only course runs currently open for enrollment or opening in the future. """
# pylint: disable=no-member
self.assertListEqual(list(self.course.active_course_runs), [])
# Create course with end date in future and enrollment_end in past.
......@@ -93,7 +95,6 @@ class CourseRunTests(TestCase):
def test_str(self):
""" Verify casting an instance to a string returns a string containing the key and title. """
course_run = self.course_run
# pylint: disable=no-member
self.assertEqual(str(course_run), '{key}: {title}'.format(key=course_run.key, title=course_run.title))
@ddt.data('title', 'short_description', 'full_description')
......@@ -125,6 +126,56 @@ class CourseRunTests(TestCase):
expected_sorted = sorted(course_runs, key=lambda course_run: course_run.key)
self.assertEqual(actual_sorted, expected_sorted)
def test_seat_types(self):
""" Verify the property returns a list of all seat types associated with the course run. """
self.assertEqual(self.course_run.seat_types, [])
seats = factories.SeatFactory.create_batch(3, course_run=self.course_run)
expected = sorted([seat.type for seat in seats])
self.assertEqual(sorted(self.course_run.seat_types), expected)
def test_image_url(self):
""" Verify the property returns the associated image's URL. """
self.assertEqual(self.course_run.image_url, self.course_run.image.src)
self.course_run.image = None
self.assertIsNone(self.course_run.image)
self.assertIsNone(self.course_run.image_url)
@ddt.data(
('obviously-wrong', None,),
(('audit',), 'audit',),
(('honor',), 'honor',),
(('credit', 'verified', 'audit',), 'credit',),
(('verified', 'honor',), 'verified',),
(('professional',), 'professional',),
(('no-id-professional',), 'professional',),
)
@ddt.unpack
def test_type(self, seat_types, expected_course_run_type):
""" Verify the property returns the appropriate type string for the CourseRun. """
for seat_type in seat_types:
factories.SeatFactory(course_run=self.course_run, type=seat_type)
self.assertEqual(self.course_run.type, expected_course_run_type)
def assert_course_run_has_no_type(self, course_run, expected_seats):
""" Asserts the given CourseRun has no type value, and a message is logged to that effect. """
with mock.patch('course_discovery.apps.course_metadata.models.logger') as mock_logger:
self.assertEqual(course_run.type, None)
mock_logger.warning.assert_called_with(
'Unable to determine type for course run [%s]. Seat types are [%s]',
course_run.key,
expected_seats
)
def test_type_with_unknown_seat_type(self):
""" Verify the property logs a warning if the CourseRun has no Seats or the Seats have an unknown seat type. """
self.assert_course_run_has_no_type(self.course_run, set())
seat_type = 'super-wrong'
factories.SeatFactory(course_run=self.course_run, type=seat_type)
self.assert_course_run_has_no_type(self.course_run, set([seat_type]))
class OrganizationTests(TestCase):
""" Tests for the `Organization` 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