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
a4bed7bd
Commit
a4bed7bd
authored
Jul 13, 2016
by
Clinton Blackburn
Committed by
GitHub
Jul 13, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #159 from edx/clintonb/programs-search
Added programs search
parents
55d6f543
4439723f
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
129 additions
and
29 deletions
+129
-29
course_discovery/apps/api/filters.py
+1
-1
course_discovery/apps/api/serializers.py
+23
-5
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
+18
-11
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/filters.py
View file @
a4bed7bd
...
...
@@ -46,7 +46,7 @@ class FacetQueryBuilderWithQueries(FacetQueryBuilder):
def
build_query
(
self
,
**
filters
):
query
=
super
(
FacetQueryBuilderWithQueries
,
self
)
.
build_query
(
**
filters
)
facet_serializer_cls
=
self
.
view
.
get_facet_serializer_class
()
query
[
'query_facets'
]
=
facet_serializer_cls
.
Meta
.
field_queries
query
[
'query_facets'
]
=
getattr
(
facet_serializer_cls
.
Meta
,
'field_queries'
,
{})
return
query
...
...
course_discovery/apps/api/serializers.py
View file @
a4bed7bd
...
...
@@ -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
()
...
...
@@ -415,7 +415,7 @@ class BaseHaystackFacetSerializer(HaystackFacetSerializer):
_abstract
=
True
def
get_fields
(
self
):
query_facet_counts
=
self
.
instance
.
pop
(
'queries'
)
query_facet_counts
=
self
.
instance
.
pop
(
'queries'
,
{}
)
field_mapping
=
super
(
BaseHaystackFacetSerializer
,
self
)
.
get_fields
()
...
...
@@ -432,7 +432,7 @@ class BaseHaystackFacetSerializer(HaystackFacetSerializer):
def
format_query_facet_data
(
self
,
query_facet_counts
):
query_data
=
{}
for
field
,
options
in
self
.
Meta
.
field_queries
.
items
():
# pylint: disable=no-member
for
field
,
options
in
getattr
(
self
.
Meta
,
'field_queries'
,
{})
.
items
():
# pylint: disable=no-member
count
=
query_facet_counts
.
get
(
field
,
0
)
if
count
:
query_data
[
field
]
=
{
...
...
@@ -468,8 +468,6 @@ class CourseFacetSerializer(BaseHaystackFacetSerializer):
class
CourseRunSearchSerializer
(
HaystackSerializer
):
content_type
=
serializers
.
CharField
(
source
=
'model_name'
)
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
COURSE_RUN_SEARCH_FIELDS
...
...
@@ -487,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
...
...
@@ -495,6 +511,7 @@ class AggregateSearchSerializer(HaystackSerializer):
serializers
=
{
CourseRunIndex
:
CourseRunSearchSerializer
,
CourseIndex
:
CourseSearchSerializer
,
ProgramIndex
:
ProgramSearchSerializer
,
}
...
...
@@ -509,4 +526,5 @@ class AggregateFacetSearchSerializer(BaseHaystackFacetSerializer):
serializers
=
{
CourseRunIndex
:
CourseRunFacetSerializer
,
CourseIndex
:
CourseFacetSerializer
,
ProgramIndex
:
ProgramFacetSerializer
,
}
course_discovery/apps/api/tests/test_serializers.py
View file @
a4bed7bd
...
...
@@ -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 @
a4bed7bd
...
...
@@ -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 @
a4bed7bd
...
...
@@ -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,13 +412,13 @@ 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
=
facet_serializer_cls
.
Meta
.
field_queries
field_queries
=
getattr
(
facet_serializer_cls
.
Meta
,
'field_queries'
,
{})
for
facet
in
self
.
request
.
query_params
.
getlist
(
'selected_query_facets'
):
query
=
field_queries
.
get
(
facet
)
...
...
@@ -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 @
a4bed7bd
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 @
a4bed7bd
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 @
a4bed7bd
...
...
@@ -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 @
a4bed7bd
{{ 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