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
cdf121f6
Commit
cdf121f6
authored
Jun 29, 2016
by
Clinton Blackburn
Committed by
GitHub
Jun 29, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Exposing additional course run fields via search API (#142)
ECOM-4747
parent
84401d0d
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
152 additions
and
34 deletions
+152
-34
course_discovery/apps/api/pagination.py
+5
-0
course_discovery/apps/api/serializers.py
+7
-5
course_discovery/apps/api/tests/test_serializers.py
+47
-3
course_discovery/apps/api/v1/tests/test_views/test_search.py
+5
-21
course_discovery/apps/api/v1/views.py
+1
-1
course_discovery/apps/course_metadata/models.py
+29
-0
course_discovery/apps/course_metadata/search_indexes.py
+3
-0
course_discovery/apps/course_metadata/tests/test_models.py
+55
-4
No files found.
course_discovery/apps/api/pagination.py
0 → 100644
View file @
cdf121f6
from
rest_framework
import
pagination
class
PageNumberPagination
(
pagination
.
PageNumberPagination
):
page_size_query_param
=
'page_size'
course_discovery/apps/api/serializers.py
View file @
cdf121f6
...
...
@@ -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'
,
)
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
cdf121f6
...
...
@@ -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
)
course_discovery/apps/api/v1/tests/test_views/test_search.py
View file @
cdf121f6
...
...
@@ -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
):
...
...
course_discovery/apps/api/v1/views.py
View file @
cdf121f6
...
...
@@ -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
...
...
course_discovery/apps/course_metadata/models.py
View file @
cdf121f6
...
...
@@ -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.
...
...
course_discovery/apps/course_metadata/search_indexes.py
View file @
cdf121f6
...
...
@@ -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
...
...
course_discovery/apps/course_metadata/tests/test_models.py
View file @
cdf121f6
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. """
...
...
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