Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
course-discovery
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
course-discovery
Commits
f9187b85
Commit
f9187b85
authored
Nov 28, 2016
by
Matthew Piatetsky
Committed by
GitHub
Nov 28, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #447 from edx/ECOM-6377
ECOM-4738 Add Typeahead endpoint to course discovery
parents
95a419d0
319ebd2c
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
184 additions
and
3 deletions
+184
-3
course_discovery/apps/api/serializers.py
+40
-0
course_discovery/apps/api/tests/test_serializers.py
+55
-1
course_discovery/apps/api/v1/tests/test_views/test_search.py
+59
-2
course_discovery/apps/api/v1/urls.py
+1
-0
course_discovery/apps/api/v1/views.py
+29
-0
No files found.
course_discovery/apps/api/serializers.py
View file @
f9187b85
...
...
@@ -913,6 +913,20 @@ class CourseRunSearchSerializer(HaystackSerializer):
index_classes
=
[
CourseRunIndex
]
class
TypeaheadCourseRunSearchSerializer
(
HaystackSerializer
):
additional_details
=
serializers
.
SerializerMethodField
()
def
get_additional_details
(
self
,
result
):
""" Value of the grey text next to the typeahead result title. """
return
result
.
org
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
[
'key'
,
'title'
,
'content_type'
]
ignore_fields
=
COMMON_IGNORED_FIELDS
index_classes
=
[
CourseRunIndex
]
class
CourseRunFacetSerializer
(
BaseHaystackFacetSerializer
):
serialize_objects
=
True
...
...
@@ -938,6 +952,21 @@ class ProgramSearchSerializer(HaystackSerializer):
index_classes
=
[
ProgramIndex
]
class
TypeaheadProgramSearchSerializer
(
HaystackSerializer
):
additional_details
=
serializers
.
SerializerMethodField
()
def
get_additional_details
(
self
,
result
):
""" Value of the grey text next to the typeahead result title. """
authoring_organizations
=
[
json
.
loads
(
org
)
for
org
in
result
.
authoring_organization_bodies
]
return
', '
.
join
([
org
[
'key'
]
for
org
in
authoring_organizations
])
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
[
'uuid'
,
'title'
,
'content_type'
,
'type'
]
ignore_fields
=
COMMON_IGNORED_FIELDS
index_classes
=
[
ProgramIndex
]
class
ProgramFacetSerializer
(
BaseHaystackFacetSerializer
):
serialize_objects
=
True
...
...
@@ -961,6 +990,17 @@ class AggregateSearchSerializer(HaystackSerializer):
}
class
TypeaheadSearchSerializer
(
HaystackSerializer
):
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
COURSE_RUN_SEARCH_FIELDS
+
PROGRAM_SEARCH_FIELDS
ignore_fields
=
COMMON_IGNORED_FIELDS
serializers
=
{
ProgramIndex
:
TypeaheadProgramSearchSerializer
,
CourseRunIndex
:
TypeaheadCourseRunSearchSerializer
,
}
class
AggregateFacetSearchSerializer
(
BaseHaystackFacetSerializer
):
serialize_objects
=
True
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
f9187b85
...
...
@@ -17,7 +17,7 @@ from course_discovery.apps.api.serializers import (
CourseRunWithProgramsSerializer
,
CourseWithProgramsSerializer
,
CorporateEndorsementSerializer
,
FAQSerializer
,
EndorsementSerializer
,
PositionSerializer
,
FlattenedCourseRunWithCourseSerializer
,
MinimalCourseSerializer
,
MinimalOrganizationSerializer
,
MinimalCourseRunSerializer
,
MinimalProgramSerializer
,
CourseSerializer
CourseSerializer
,
TypeaheadCourseRunSearchSerializer
,
TypeaheadProgramSearchSerializer
)
from
course_discovery.apps.catalogs.tests.factories
import
CatalogFactory
from
course_discovery.apps.core.models
import
User
...
...
@@ -1099,3 +1099,57 @@ class ProgramSearchSerializerTests(TestCase):
expected
=
self
.
_create_expected_data
(
program
)
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
class
TypeaheadCourseRunSearchSerializerTests
(
TestCase
):
def
test_data
(
self
):
course_run
=
CourseRunFactory
()
serialized_course
=
self
.
serialize_course_run
(
course_run
)
course_run_key
=
CourseKey
.
from_string
(
course_run
.
key
)
expected
=
{
'key'
:
course_run
.
key
,
'title'
:
course_run
.
title
,
'content_type'
:
'courserun'
,
'additional_details'
:
course_run_key
.
org
}
self
.
assertDictEqual
(
serialized_course
.
data
,
expected
)
def
serialize_course_run
(
self
,
course_run
):
""" Serializes the given `CourseRun` as a typeahead result. """
result
=
SearchQuerySet
()
.
models
(
CourseRun
)
.
filter
(
key
=
course_run
.
key
)[
0
]
serializer
=
TypeaheadCourseRunSearchSerializer
(
result
)
return
serializer
class
TypeaheadProgramSearchSerializerTests
(
TestCase
):
def
_create_expected_data
(
self
,
program
):
return
{
'uuid'
:
str
(
program
.
uuid
),
'title'
:
program
.
title
,
'type'
:
program
.
type
.
name
,
'content_type'
:
'program'
,
'additional_details'
:
program
.
authoring_organizations
.
first
()
.
key
}
def
test_data
(
self
):
authoring_organization
=
OrganizationFactory
()
program
=
ProgramFactory
(
authoring_organizations
=
[
authoring_organization
])
serialized_program
=
self
.
serialize_program
(
program
)
expected
=
self
.
_create_expected_data
(
program
)
self
.
assertDictEqual
(
serialized_program
.
data
,
expected
)
def
test_data_multiple_authoring_organizations
(
self
):
authoring_organizations
=
OrganizationFactory
.
create_batch
(
3
)
program
=
ProgramFactory
(
authoring_organizations
=
authoring_organizations
)
serialized_program
=
self
.
serialize_program
(
program
)
expected
=
', '
.
join
([
org
.
key
for
org
in
authoring_organizations
])
self
.
assertEqual
(
serialized_program
.
data
[
'additional_details'
],
expected
)
def
serialize_program
(
self
,
program
):
""" Serializes the given `Program` as a typeahead result. """
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
serializer
=
TypeaheadProgramSearchSerializer
(
result
)
return
serializer
course_discovery/apps/api/v1/tests/test_views/test_search.py
View file @
f9187b85
...
...
@@ -8,12 +8,15 @@ 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
,
ProgramSearchSerializer
from
course_discovery.apps.api.serializers
import
(
CourseRunSearchSerializer
,
ProgramSearchSerializer
,
TypeaheadCourseRunSearchSerializer
,
TypeaheadProgramSearchSerializer
)
from
course_discovery.apps.api.v1.views
import
RESULT_COUNT
from
course_discovery.apps.core.tests.factories
import
UserFactory
,
USER_PASSWORD
,
PartnerFactory
from
course_discovery.apps.core.tests.mixins
import
ElasticsearchTestMixin
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
ProgramStatus
from
course_discovery.apps.course_metadata.models
import
CourseRun
,
Program
from
course_discovery.apps.course_metadata.tests.factories
import
CourseRunFactory
,
ProgramFactory
from
course_discovery.apps.course_metadata.tests.factories
import
CourseRunFactory
,
ProgramFactory
,
OrganizationFactory
class
SerializationMixin
:
...
...
@@ -26,6 +29,22 @@ class SerializationMixin:
return
ProgramSearchSerializer
(
result
)
.
data
class
TypeaheadSerializationMixin
:
def
serialize_course_run
(
self
,
course_run
):
result
=
SearchQuerySet
()
.
models
(
CourseRun
)
.
filter
(
key
=
course_run
.
key
)[
0
]
data
=
TypeaheadCourseRunSearchSerializer
(
result
)
.
data
# Items are grouped by content type so we don't need it in the response
data
.
pop
(
'content_type'
)
return
data
def
serialize_program
(
self
,
program
):
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
data
=
TypeaheadProgramSearchSerializer
(
result
)
.
data
# Items are grouped by content type so we don't need it in the response
data
.
pop
(
'content_type'
)
return
data
class
LoginMixin
:
def
setUp
(
self
):
super
(
LoginMixin
,
self
)
.
setUp
()
...
...
@@ -279,3 +298,41 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertListEqual
(
response_data
[
'objects'
][
'results'
],
[
self
.
serialize_course_run
(
course_run
),
self
.
serialize_program
(
program
)])
class
TypeaheadSearchViewSet
(
TypeaheadSerializationMixin
,
LoginMixin
,
APITestCase
):
path
=
reverse
(
'api:v1:search-typeahead-list'
)
def
get_typeahead_response
(
self
):
return
self
.
client
.
get
(
self
.
path
)
def
test_typeahead
(
self
):
""" Test typeahead response. """
course_run
=
CourseRunFactory
()
program
=
ProgramFactory
()
response
=
self
.
get_typeahead_response
()
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertDictEqual
(
response_data
,
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]})
def
test_typeahead_multiple_results
(
self
):
""" Test typeahead response with max number of course_runs and programs. """
CourseRunFactory
.
create_batch
(
RESULT_COUNT
+
1
)
ProgramFactory
.
create_batch
(
RESULT_COUNT
+
1
)
response
=
self
.
get_typeahead_response
()
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertEqual
(
len
(
response_data
[
'course_runs'
]),
RESULT_COUNT
)
self
.
assertEqual
(
len
(
response_data
[
'programs'
]),
RESULT_COUNT
)
def
test_typeahead_multiple_authoring_organizations
(
self
):
""" Test typeahead response with multiple authoring organizations. """
authoring_organizations
=
OrganizationFactory
.
create_batch
(
3
)
course_run
=
CourseRunFactory
(
authoring_organizations
=
authoring_organizations
)
program
=
ProgramFactory
(
authoring_organizations
=
authoring_organizations
)
response
=
self
.
get_typeahead_response
()
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertDictEqual
(
response_data
,
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]})
course_discovery/apps/api/v1/urls.py
View file @
f9187b85
...
...
@@ -18,6 +18,7 @@ router.register(r'course_runs', views.CourseRunViewSet, base_name='course_run')
router
.
register
(
r'management'
,
views
.
ManagementViewSet
,
base_name
=
'management'
)
router
.
register
(
r'programs'
,
views
.
ProgramViewSet
,
base_name
=
'program'
)
router
.
register
(
r'search/all'
,
views
.
AggregateSearchViewSet
,
base_name
=
'search-all'
)
router
.
register
(
r'search/typeahead'
,
views
.
TypeaheadSearchViewSet
,
base_name
=
'search-typeahead'
)
router
.
register
(
r'search/courses'
,
views
.
CourseSearchViewSet
,
base_name
=
'search-courses'
)
router
.
register
(
r'search/course_runs'
,
views
.
CourseRunSearchViewSet
,
base_name
=
'search-course_runs'
)
router
.
register
(
r'search/programs'
,
views
.
ProgramSearchViewSet
,
base_name
=
'search-programs'
)
...
...
course_discovery/apps/api/v1/views.py
View file @
f9187b85
...
...
@@ -37,6 +37,8 @@ from course_discovery.apps.course_metadata.models import Course, CourseRun, Part
logger
=
logging
.
getLogger
(
__name__
)
User
=
get_user_model
()
RESULT_COUNT
=
3
def
get_query_param
(
request
,
name
):
"""
...
...
@@ -676,3 +678,30 @@ class AggregateSearchViewSet(BaseHaystackViewSet):
""" Search all content types. """
facet_serializer_class
=
serializers
.
AggregateFacetSearchSerializer
serializer_class
=
serializers
.
AggregateSearchSerializer
class
TypeaheadSearchViewSet
(
BaseHaystackViewSet
):
"""
Typeahead for courses and programs.
"""
serializer_class
=
serializers
.
TypeaheadSearchSerializer
index_models
=
(
CourseRun
,
Program
,)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
response
=
super
(
TypeaheadSearchViewSet
,
self
)
.
list
(
request
,
*
args
,
**
kwargs
)
results
=
response
.
data
[
'results'
]
course_runs
,
programs
=
[],
[]
for
item
in
results
:
# Items are grouped by content type so we don't need it in the response
item_type
=
item
.
pop
(
'content_type'
,
None
)
programs_length
=
len
(
programs
)
course_run_length
=
len
(
course_runs
)
if
item_type
==
'courserun'
and
course_run_length
<
RESULT_COUNT
:
course_runs
.
append
(
item
)
elif
item_type
==
'program'
and
programs_length
<
RESULT_COUNT
:
programs
.
append
(
item
)
elif
programs_length
==
RESULT_COUNT
and
course_run_length
==
RESULT_COUNT
:
break
response
.
data
=
{
'course_runs'
:
course_runs
,
'programs'
:
programs
}
return
response
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment