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
4439723f
Commit
4439723f
authored
Jul 12, 2016
by
Clinton Blackburn
Committed by
Clinton Blackburn
Jul 13, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added programs search
ECOM-4801
parent
c89b6cb3
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
125 additions
and
23 deletions
+125
-23
course_discovery/apps/api/serializers.py
+21
-1
course_discovery/apps/api/tests/test_serializers.py
+29
-4
course_discovery/apps/api/v1/urls.py
+1
-0
course_discovery/apps/api/v1/views.py
+17
-10
course_discovery/apps/course_metadata/models.py
+10
-0
course_discovery/apps/course_metadata/search_indexes.py
+25
-6
course_discovery/apps/course_metadata/tests/test_models.py
+13
-2
course_discovery/templates/search/indexes/course_metadata/program_text.txt
+9
-0
No files found.
course_discovery/apps/api/serializers.py
View file @
4439723f
...
...
@@ -12,7 +12,7 @@ from course_discovery.apps.catalogs.models import Catalog
from
course_discovery.apps.course_metadata.models
import
(
Course
,
CourseRun
,
Image
,
Organization
,
Person
,
Prerequisite
,
Seat
,
Subject
,
Video
)
from
course_discovery.apps.course_metadata.search_indexes
import
CourseIndex
,
CourseRunIndex
from
course_discovery.apps.course_metadata.search_indexes
import
CourseIndex
,
CourseRunIndex
,
ProgramIndex
User
=
get_user_model
()
...
...
@@ -485,6 +485,24 @@ class CourseRunFacetSerializer(BaseHaystackFacetSerializer):
ignore_fields
=
COMMON_IGNORED_FIELDS
class
ProgramSearchSerializer
(
HaystackSerializer
):
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
(
'uuid'
,
'name'
,
'subtitle'
,
'category'
,
'marketing_url'
,
'organizations'
,
'text'
,)
ignore_fields
=
COMMON_IGNORED_FIELDS
index_classes
=
[
ProgramIndex
]
class
ProgramFacetSerializer
(
BaseHaystackFacetSerializer
):
serialize_objects
=
True
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
(
'uuid'
,
'name'
,
'subtitle'
,
'category'
,
'marketing_url'
,
'organizations'
,
'text'
,)
ignore_fields
=
COMMON_IGNORED_FIELDS
index_classes
=
[
ProgramIndex
]
class
AggregateSearchSerializer
(
HaystackSerializer
):
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
...
...
@@ -493,6 +511,7 @@ class AggregateSearchSerializer(HaystackSerializer):
serializers
=
{
CourseRunIndex
:
CourseRunSearchSerializer
,
CourseIndex
:
CourseSearchSerializer
,
ProgramIndex
:
ProgramSearchSerializer
,
}
...
...
@@ -507,4 +526,5 @@ class AggregateFacetSearchSerializer(BaseHaystackFacetSerializer):
serializers
=
{
CourseRunIndex
:
CourseRunFacetSerializer
,
CourseIndex
:
CourseFacetSerializer
,
ProgramIndex
:
ProgramFacetSerializer
,
}
course_discovery/apps/api/tests/test_serializers.py
View file @
4439723f
...
...
@@ -11,14 +11,16 @@ from course_discovery.apps.api.serializers import (
CatalogSerializer
,
CourseSerializer
,
CourseRunSerializer
,
ContainedCoursesSerializer
,
ImageSerializer
,
SubjectSerializer
,
PrerequisiteSerializer
,
VideoSerializer
,
OrganizationSerializer
,
SeatSerializer
,
PersonSerializer
,
AffiliateWindowSerializer
,
ContainedCourseRunsSerializer
,
CourseRunSearchSerializer
)
CourseRunSearchSerializer
,
ProgramSearchSerializer
)
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.models
import
CourseRun
,
Program
from
course_discovery.apps.course_metadata.search_indexes
import
OrganizationsMixin
from
course_discovery.apps.course_metadata.tests.factories
import
(
CourseFactory
,
CourseRunFactory
,
SubjectFactory
,
PrerequisiteFactory
,
ImageFactory
,
VideoFactory
,
OrganizationFactory
,
PersonFactory
,
Seat
Factory
CourseFactory
,
CourseRunFactory
,
SubjectFactory
,
PrerequisiteFactory
,
ImageFactory
,
VideoFactory
,
OrganizationFactory
,
PersonFactory
,
SeatFactory
,
Program
Factory
)
...
...
@@ -358,3 +360,26 @@ class CourseRunSearchSerializerTests(TestCase):
'type'
:
course_run
.
type
,
}
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
class
ProgramSearchSerializerTests
(
TestCase
):
def
test_data
(
self
):
program
=
ProgramFactory
()
organization
=
OrganizationFactory
()
program
.
organizations
.
add
(
organization
)
program
.
save
()
# NOTE: This serializer expects SearchQuerySet results, so we run a search on the newly-created object
# to generate such a result.
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
serializer
=
ProgramSearchSerializer
(
result
)
expected
=
{
'uuid'
:
str
(
program
.
uuid
),
'name'
:
program
.
name
,
'subtitle'
:
program
.
subtitle
,
'category'
:
program
.
category
,
'marketing_url'
:
program
.
marketing_url
,
'organizations'
:
[
OrganizationsMixin
.
format_organization
(
organization
)],
}
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
course_discovery/apps/api/v1/urls.py
View file @
4439723f
...
...
@@ -19,5 +19,6 @@ router.register(r'management', views.ManagementViewSet, base_name='management')
router
.
register
(
r'search/all'
,
views
.
AggregateSearchViewSet
,
base_name
=
'search-all'
)
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'
)
urlpatterns
+=
router
.
urls
course_discovery/apps/api/v1/views.py
View file @
4439723f
...
...
@@ -29,7 +29,7 @@ from course_discovery.apps.api.renderers import AffiliateWindowXMLRenderer, Cour
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.core.utils
import
SearchQuerySetWrapper
from
course_discovery.apps.course_metadata.constants
import
COURSE_ID_REGEX
,
COURSE_RUN_ID_REGEX
from
course_discovery.apps.course_metadata.models
import
Course
,
CourseRun
,
Seat
from
course_discovery.apps.course_metadata.models
import
Course
,
CourseRun
,
Seat
,
Program
logger
=
logging
.
getLogger
(
__name__
)
User
=
get_user_model
()
...
...
@@ -360,14 +360,14 @@ class AffiliateWindowViewSet(viewsets.ViewSet):
return
Response
(
serializer
.
data
)
class
Base
Course
HaystackViewSet
(
FacetMixin
,
HaystackViewSet
):
class
BaseHaystackViewSet
(
FacetMixin
,
HaystackViewSet
):
document_uid_field
=
'key'
facet_filter_backends
=
[
HaystackFacetFilterWithQueries
,
HaystackFilter
]
load_all
=
True
lookup_field
=
'key'
permission_classes
=
(
IsAuthenticated
,)
# NOTE: We use PageNumberPagination because drf-haytack's facet serializer relies on the page_query_param
# NOTE: We use PageNumberPagination because drf-hay
s
tack's facet serializer relies on the page_query_param
# attribute, and it is more appropriate for search results than our default limit-offset pagination.
pagination_class
=
PageNumberPagination
...
...
@@ -382,7 +382,7 @@ class BaseCourseHaystackViewSet(FacetMixin, HaystackViewSet):
type: string
required: false
"""
return
super
(
Base
Course
HaystackViewSet
,
self
)
.
list
(
request
,
*
args
,
**
kwargs
)
return
super
(
BaseHaystackViewSet
,
self
)
.
list
(
request
,
*
args
,
**
kwargs
)
@list_route
(
methods
=
[
"get"
],
url_path
=
"facets"
)
def
facets
(
self
,
request
):
...
...
@@ -412,10 +412,10 @@ class BaseCourseHaystackViewSet(FacetMixin, HaystackViewSet):
pytype: str
required: false
"""
return
super
(
Base
Course
HaystackViewSet
,
self
)
.
facets
(
request
)
return
super
(
BaseHaystackViewSet
,
self
)
.
facets
(
request
)
def
filter_facet_queryset
(
self
,
queryset
):
queryset
=
super
(
Base
Course
HaystackViewSet
,
self
)
.
filter_facet_queryset
(
queryset
)
queryset
=
super
(
BaseHaystackViewSet
,
self
)
.
filter_facet_queryset
(
queryset
)
facet_serializer_cls
=
self
.
get_facet_serializer_class
()
field_queries
=
getattr
(
facet_serializer_cls
.
Meta
,
'field_queries'
,
{})
...
...
@@ -431,20 +431,27 @@ class BaseCourseHaystackViewSet(FacetMixin, HaystackViewSet):
return
queryset
class
CourseSearchViewSet
(
Base
Course
HaystackViewSet
):
class
CourseSearchViewSet
(
BaseHaystackViewSet
):
facet_serializer_class
=
serializers
.
CourseFacetSerializer
index_models
=
(
Course
,)
serializer_class
=
serializers
.
CourseSearchSerializer
class
CourseRunSearchViewSet
(
Base
Course
HaystackViewSet
):
class
CourseRunSearchViewSet
(
BaseHaystackViewSet
):
facet_serializer_class
=
serializers
.
CourseRunFacetSerializer
index_models
=
(
CourseRun
,)
serializer_class
=
serializers
.
CourseRunSearchSerializer
# TODO Remove the detail routes. They don't work, and make no sense here given that we cannot specify the type.
class
AggregateSearchViewSet
(
BaseCourseHaystackViewSet
):
class
ProgramSearchViewSet
(
BaseHaystackViewSet
):
document_uid_field
=
'uuid'
lookup_field
=
'uuid'
facet_serializer_class
=
serializers
.
ProgramFacetSerializer
index_models
=
(
Program
,)
serializer_class
=
serializers
.
ProgramSearchSerializer
class
AggregateSearchViewSet
(
BaseHaystackViewSet
):
""" Search all content types. """
facet_serializer_class
=
serializers
.
AggregateFacetSearchSerializer
serializer_class
=
serializers
.
AggregateSearchSerializer
course_discovery/apps/course_metadata/models.py
View file @
4439723f
import
datetime
import
logging
from
urllib.parse
import
urljoin
from
uuid
import
uuid4
import
pytz
from
django.conf
import
settings
from
django.db
import
models
from
django.db.models.query_utils
import
Q
from
django.utils.translation
import
ugettext_lazy
as
_
...
...
@@ -425,5 +427,13 @@ class Program(TimeStampedModel):
organizations
=
models
.
ManyToManyField
(
Organization
,
blank
=
True
)
@property
def
marketing_url
(
self
):
if
self
.
marketing_slug
:
path
=
'{category}/{slug}'
.
format
(
category
=
self
.
category
,
slug
=
self
.
marketing_slug
)
return
urljoin
(
settings
.
MARKETING_URL_ROOT
,
path
)
return
None
def
__str__
(
self
):
return
self
.
name
course_discovery/apps/course_metadata/search_indexes.py
View file @
4439723f
from
haystack
import
indexes
from
opaque_keys.edx.keys
import
CourseKey
from
course_discovery.apps.course_metadata.models
import
Course
,
CourseRun
from
course_discovery.apps.course_metadata.models
import
Course
,
CourseRun
,
Program
class
OrganizationsMixin
:
@classmethod
def
format_organization
(
cls
,
organization
):
return
'{key}: {name}'
.
format
(
key
=
organization
.
key
,
name
=
organization
.
name
)
def
prepare_organizations
(
self
,
obj
):
return
[
self
.
format_organization
(
organization
)
for
organization
in
obj
.
organizations
.
all
()]
class
BaseIndex
(
indexes
.
SearchIndex
):
...
...
@@ -23,7 +32,7 @@ class BaseIndex(indexes.SearchIndex):
return
self
.
model
.
objects
.
all
()
class
BaseCourseIndex
(
BaseIndex
):
class
BaseCourseIndex
(
OrganizationsMixin
,
BaseIndex
):
key
=
indexes
.
CharField
(
model_attr
=
'key'
,
stored
=
True
)
title
=
indexes
.
CharField
(
model_attr
=
'title'
)
short_description
=
indexes
.
CharField
(
model_attr
=
'short_description'
,
null
=
True
)
...
...
@@ -31,10 +40,6 @@ class BaseCourseIndex(BaseIndex):
subjects
=
indexes
.
MultiValueField
(
faceted
=
True
)
organizations
=
indexes
.
MultiValueField
(
faceted
=
True
)
def
prepare_organizations
(
self
,
obj
):
return
[
'{key}: {name}'
.
format
(
key
=
organization
.
key
,
name
=
organization
.
name
)
for
organization
in
obj
.
organizations
.
all
()]
def
prepare_subjects
(
self
,
obj
):
return
[
subject
.
name
for
subject
in
obj
.
subjects
.
all
()]
...
...
@@ -97,3 +102,17 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
def
prepare_transcript_languages
(
self
,
obj
):
return
[
self
.
_prepare_language
(
language
)
for
language
in
obj
.
transcript_languages
.
all
()]
class
ProgramIndex
(
OrganizationsMixin
,
BaseIndex
,
indexes
.
Indexable
):
model
=
Program
uuid
=
indexes
.
CharField
(
model_attr
=
'uuid'
)
name
=
indexes
.
CharField
(
model_attr
=
'name'
)
subtitle
=
indexes
.
CharField
(
model_attr
=
'subtitle'
)
category
=
indexes
.
CharField
(
model_attr
=
'category'
,
faceted
=
True
)
marketing_url
=
indexes
.
CharField
(
model_attr
=
'marketing_url'
,
null
=
True
)
organizations
=
indexes
.
MultiValueField
(
faceted
=
True
)
def
prepare_content_type
(
self
,
obj
):
return
'program_{category}'
.
format
(
category
=
obj
.
category
)
course_discovery/apps/course_metadata/tests/test_models.py
View file @
4439723f
...
...
@@ -3,6 +3,7 @@ import datetime
import
ddt
import
mock
import
pytz
from
django.conf
import
settings
from
django.test
import
TestCase
from
course_discovery.apps.core.utils
import
SearchQuerySetWrapper
...
...
@@ -243,6 +244,16 @@ class ProgramTests(TestCase):
def
test_str
(
self
):
"""Verify that a program is properly converted to a str."""
program
=
factories
.
ProgramFactory
()
program_str
=
program
.
name
self
.
assertEqual
(
str
(
program
),
program
.
name
)
self
.
assertEqual
(
str
(
program
),
program_str
)
def
test_marketing_url
(
self
):
""" Verify the property creates a complete marketing URL. """
program
=
factories
.
ProgramFactory
()
expected
=
'{root}/{category}/{slug}'
.
format
(
root
=
settings
.
MARKETING_URL_ROOT
.
strip
(
'/'
),
category
=
program
.
category
,
slug
=
program
.
marketing_slug
)
self
.
assertEqual
(
program
.
marketing_url
,
expected
)
def
test_marketing_url_without_slug
(
self
):
""" Verify the property returns None if the Program has no marketing_slug set. """
program
=
factories
.
ProgramFactory
(
marketing_slug
=
''
)
self
.
assertIsNone
(
program
.
marketing_url
)
course_discovery/templates/search/indexes/course_metadata/program_text.txt
0 → 100644
View file @
4439723f
{{ object.uuid }}
{{ object.name }}
{{ object.category }}
{{ object.status }}
{{ object.marketing_slug|default:'' }}
{% for organization in object.organizations.all %}
{{ organization.key }}: {{ organization.name }}
{% endfor %}
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