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
138f1434
Commit
138f1434
authored
Jun 27, 2016
by
Clinton Blackburn
Committed by
GitHub
Jun 27, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #129 from edx/clintonb/search-api
Search: courses and course runs
parents
65cbb2de
71849f13
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
440 additions
and
82 deletions
+440
-82
course_discovery/apps/api/serializers.py
+101
-2
course_discovery/apps/api/v1/tests/test_views/test_management.py
+29
-16
course_discovery/apps/api/v1/tests/test_views/test_search.py
+99
-0
course_discovery/apps/api/v1/urls.py
+4
-2
course_discovery/apps/api/v1/views.py
+97
-26
course_discovery/apps/course_metadata/models.py
+8
-0
course_discovery/apps/course_metadata/search_indexes.py
+44
-35
course_discovery/apps/course_metadata/views.py
+5
-1
course_discovery/templates/demo/query_preview.html
+0
-0
course_discovery/templates/demo/search.html
+48
-0
course_discovery/templates/search/indexes/course_metadata/courserun_text.txt
+4
-0
requirements/base.txt
+1
-0
No files found.
course_discovery/apps/api/serializers.py
View file @
138f1434
# pylint: disable=abstract-method
import
datetime
from
urllib.parse
import
urlencode
from
urllib.parse
import
urlencode
from
django.contrib.auth
import
get_user_model
from
django.contrib.auth
import
get_user_model
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.translation
import
ugettext_lazy
as
_
from
drf_haystack.serializers
import
HaystackSerializer
,
HaystackFacetSerializer
from
rest_framework
import
serializers
from
rest_framework
import
serializers
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.course_metadata.models
import
(
from
course_discovery.apps.course_metadata.models
import
(
Course
,
CourseRun
,
Image
,
Organization
,
Person
,
Prerequisite
,
Seat
,
Subject
,
Video
Course
,
CourseRun
,
Image
,
Organization
,
Person
,
Prerequisite
,
Seat
,
Subject
,
Video
)
)
from
course_discovery.apps.course_metadata.search_indexes
import
CourseIndex
,
CourseRunIndex
User
=
get_user_model
()
User
=
get_user_model
()
COMMON_IGNORED_FIELDS
=
(
'text'
,)
COMMON_SEARCH_FIELD_ALIASES
=
{
'q'
:
'text'
,
}
COURSE_RUN_FACET_FIELD_OPTIONS
=
{
'level_type'
:
{},
'organizations'
:
{},
'prerequisites'
:
{},
'subjects'
:
{},
'language'
:
{},
'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
,
},
'content_type'
:
{},
}
COURSE_RUN_SEARCH_FIELDS
=
(
'key'
,
'title'
,
'short_description'
,
'full_description'
,
'start'
,
'end'
,
'enrollment_start'
,
'enrollment_end'
,
'pacing_type'
,
'language'
,
'transcript_languages'
,
'marketing_url'
,
'text'
,
)
def
get_marketing_url_for_user
(
user
,
marketing_url
):
def
get_marketing_url_for_user
(
user
,
marketing_url
):
"""
"""
...
@@ -47,12 +77,14 @@ class NamedModelSerializer(serializers.ModelSerializer):
...
@@ -47,12 +77,14 @@ class NamedModelSerializer(serializers.ModelSerializer):
class
SubjectSerializer
(
NamedModelSerializer
):
class
SubjectSerializer
(
NamedModelSerializer
):
"""Serializer for the ``Subject`` model."""
"""Serializer for the ``Subject`` model."""
class
Meta
(
NamedModelSerializer
.
Meta
):
class
Meta
(
NamedModelSerializer
.
Meta
):
model
=
Subject
model
=
Subject
class
PrerequisiteSerializer
(
NamedModelSerializer
):
class
PrerequisiteSerializer
(
NamedModelSerializer
):
"""Serializer for the ``Prerequisite`` model."""
"""Serializer for the ``Prerequisite`` model."""
class
Meta
(
NamedModelSerializer
.
Meta
):
class
Meta
(
NamedModelSerializer
.
Meta
):
model
=
Prerequisite
model
=
Prerequisite
...
@@ -169,7 +201,7 @@ class CourseRunSerializer(TimestampModelSerializer):
...
@@ -169,7 +201,7 @@ class CourseRunSerializer(TimestampModelSerializer):
return
get_marketing_url_for_user
(
self
.
context
[
'request'
]
.
user
,
obj
.
marketing_url
)
return
get_marketing_url_for_user
(
self
.
context
[
'request'
]
.
user
,
obj
.
marketing_url
)
class
ContainedCourseRunsSerializer
(
serializers
.
Serializer
):
# pylint: disable=abstract-method
class
ContainedCourseRunsSerializer
(
serializers
.
Serializer
):
"""Serializer used to represent course runs contained by a catalog."""
"""Serializer used to represent course runs contained by a catalog."""
course_runs
=
serializers
.
DictField
(
course_runs
=
serializers
.
DictField
(
child
=
serializers
.
BooleanField
(),
child
=
serializers
.
BooleanField
(),
...
@@ -207,7 +239,7 @@ class CourseSerializerExcludingClosedRuns(CourseSerializer):
...
@@ -207,7 +239,7 @@ class CourseSerializerExcludingClosedRuns(CourseSerializer):
course_runs
=
CourseRunSerializer
(
many
=
True
,
source
=
'active_course_runs'
)
course_runs
=
CourseRunSerializer
(
many
=
True
,
source
=
'active_course_runs'
)
class
ContainedCoursesSerializer
(
serializers
.
Serializer
):
# pylint: disable=abstract-method
class
ContainedCoursesSerializer
(
serializers
.
Serializer
):
"""Serializer used to represent courses contained by a catalog."""
"""Serializer used to represent courses contained by a catalog."""
courses
=
serializers
.
DictField
(
courses
=
serializers
.
DictField
(
child
=
serializers
.
BooleanField
(),
child
=
serializers
.
BooleanField
(),
...
@@ -330,3 +362,70 @@ class FlattenedCourseRunWithCourseSerializer(CourseRunSerializer):
...
@@ -330,3 +362,70 @@ class FlattenedCourseRunWithCourseSerializer(CourseRunSerializer):
def
get_course_key
(
self
,
obj
):
def
get_course_key
(
self
,
obj
):
return
obj
.
course
.
key
return
obj
.
course
.
key
class
CourseSearchSerializer
(
HaystackSerializer
):
content_type
=
serializers
.
CharField
(
source
=
'model_name'
)
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
(
'key'
,
'title'
,
'short_description'
,
'full_description'
,
'text'
,)
ignore_fields
=
COMMON_IGNORED_FIELDS
index_classes
=
[
CourseIndex
]
class
CourseFacetSerializer
(
HaystackFacetSerializer
):
serialize_objects
=
True
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
field_options
=
{
'level_type'
:
{},
'organizations'
:
{},
'prerequisites'
:
{},
'subjects'
:
{},
}
ignore_fields
=
COMMON_IGNORED_FIELDS
class
CourseRunSearchSerializer
(
HaystackSerializer
):
content_type
=
serializers
.
CharField
(
source
=
'model_name'
)
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
COURSE_RUN_SEARCH_FIELDS
ignore_fields
=
COMMON_IGNORED_FIELDS
index_classes
=
[
CourseRunIndex
]
class
CourseRunFacetSerializer
(
HaystackFacetSerializer
):
serialize_objects
=
True
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
field_options
=
COURSE_RUN_FACET_FIELD_OPTIONS
ignore_fields
=
COMMON_IGNORED_FIELDS
class
AggregateSearchSerializer
(
HaystackSerializer
):
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
COURSE_RUN_SEARCH_FIELDS
ignore_fields
=
COMMON_IGNORED_FIELDS
serializers
=
{
CourseRunIndex
:
CourseRunSearchSerializer
,
CourseIndex
:
CourseSearchSerializer
,
}
class
AggregateFacetSearchSerializer
(
HaystackFacetSerializer
):
serialize_objects
=
True
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
field_options
=
COURSE_RUN_FACET_FIELD_OPTIONS
ignore_fields
=
COMMON_IGNORED_FIELDS
serializers
=
{
CourseRunIndex
:
CourseRunFacetSerializer
,
CourseIndex
:
CourseFacetSerializer
,
}
course_discovery/apps/api/v1/tests/test_views/test_management.py
View file @
138f1434
...
@@ -5,13 +5,13 @@ from rest_framework.test import APITestCase
...
@@ -5,13 +5,13 @@ from rest_framework.test import APITestCase
from
course_discovery.apps.core.tests.factories
import
UserFactory
from
course_discovery.apps.core.tests.factories
import
UserFactory
class
RefreshCourseMetadataTests
(
APITestCase
):
class
ManagementCommandViewTestMixin
(
object
):
""" Tests for the refresh_course_metadata management endpoint. """
call_command_path
=
None
path
=
reverse
(
'api:v1:management-refresh-course-metadata'
)
command_name
=
None
call_command_path
=
'course_discovery.apps.api.v1.views.call_command'
path
=
None
def
setUp
(
self
):
def
setUp
(
self
):
super
(
RefreshCourseMetadataTests
,
self
)
.
setUp
()
super
(
ManagementCommandViewTestMixin
,
self
)
.
setUp
()
self
.
superuser
=
UserFactory
(
is_superuser
=
True
)
self
.
superuser
=
UserFactory
(
is_superuser
=
True
)
self
.
client
.
force_authenticate
(
self
.
superuser
)
# pylint: disable=no-member
self
.
client
.
force_authenticate
(
self
.
superuser
)
# pylint: disable=no-member
...
@@ -20,12 +20,8 @@ class RefreshCourseMetadataTests(APITestCase):
...
@@ -20,12 +20,8 @@ class RefreshCourseMetadataTests(APITestCase):
response
=
self
.
client
.
post
(
self
.
path
)
response
=
self
.
client
.
post
(
self
.
path
)
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_superuser_required
(
self
):
def
test_non_superusers_denied
(
self
):
""" Verify only superusers can access the endpoint. """
""" Verify access is denied to non-superusers. """
with
mock
.
patch
(
self
.
call_command_path
,
return_value
=
None
):
response
=
self
.
client
.
post
(
self
.
path
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Anonymous user
# Anonymous user
self
.
client
.
logout
()
self
.
client
.
logout
()
self
.
assert_access_forbidden
()
self
.
assert_access_forbidden
()
...
@@ -42,8 +38,8 @@ class RefreshCourseMetadataTests(APITestCase):
...
@@ -42,8 +38,8 @@ class RefreshCourseMetadataTests(APITestCase):
self
.
assert_successful_response
(
'abc123'
)
self
.
assert_successful_response
(
'abc123'
)
def
assert_successful_response
(
self
,
access_token
=
None
):
def
assert_successful_response
(
self
,
access_token
=
None
):
""" Asserts the endpoint called the
refresh_course_metadata management command with the correct arguments,
""" Asserts the endpoint called the
correct management command with the correct arguments, and the endpoint
and the endpoint
returns HTTP 200 with text/plain content type. """
returns HTTP 200 with text/plain content type. """
data
=
{
'access_token'
:
access_token
}
if
access_token
else
None
data
=
{
'access_token'
:
access_token
}
if
access_token
else
None
with
mock
.
patch
(
self
.
call_command_path
,
return_value
=
None
)
as
mocked_call_command
:
with
mock
.
patch
(
self
.
call_command_path
,
return_value
=
None
)
as
mocked_call_command
:
response
=
self
.
client
.
post
(
self
.
path
,
data
)
response
=
self
.
client
.
post
(
self
.
path
,
data
)
...
@@ -55,9 +51,26 @@ class RefreshCourseMetadataTests(APITestCase):
...
@@ -55,9 +51,26 @@ class RefreshCourseMetadataTests(APITestCase):
expected
=
{
expected
=
{
'settings'
:
'course_discovery.settings.test'
'settings'
:
'course_discovery.settings.test'
}
}
if
access_token
:
expected
[
'access_token'
]
=
access_token
self
.
assertTrue
(
mocked_call_command
.
called
)
self
.
assertTrue
(
mocked_call_command
.
called
)
self
.
assertEqual
(
args
[
0
],
'refresh_course_metadata'
)
self
.
assertEqual
(
args
[
0
],
self
.
command_name
)
self
.
assertDictContainsSubset
(
expected
,
kwargs
)
self
.
assertDictContainsSubset
(
expected
,
kwargs
)
class
RefreshCourseMetadataTests
(
ManagementCommandViewTestMixin
,
APITestCase
):
""" Tests for the refresh_course_metadata management endpoint. """
call_command_path
=
'course_discovery.apps.api.v1.views.call_command'
command_name
=
'refresh_course_metadata'
path
=
reverse
(
'api:v1:management-refresh-course-metadata'
)
def
test_success_response
(
self
):
""" Verify a successful response calls the management command and returns the plain text output. """
super
(
RefreshCourseMetadataTests
,
self
)
.
test_success_response
()
self
.
assert_successful_response
(
access_token
=
'abc123'
)
class
UpdateIndexTests
(
ManagementCommandViewTestMixin
,
APITestCase
):
""" Tests for the update_index management endpoint. """
call_command_path
=
'course_discovery.apps.api.v1.views.call_command'
command_name
=
'update_index'
path
=
reverse
(
'api:v1:management-update-index'
)
course_discovery/apps/api/v1/tests/test_views/test_search.py
0 → 100644
View file @
138f1434
import
json
import
urllib.parse
import
ddt
from
django.core.urlresolvers
import
reverse
from
rest_framework.test
import
APITestCase
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.tests.factories
import
CourseRunFactory
@ddt.ddt
class
CourseRunSearchViewSetTests
(
ElasticsearchTestMixin
,
APITestCase
):
""" Tests for CourseRunSearchViewSet. """
faceted_path
=
reverse
(
'api:v1:search-course_runs-facets'
)
list_path
=
reverse
(
'api:v1:search-course_runs-list'
)
def
setUp
(
self
):
super
(
CourseRunSearchViewSetTests
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
USER_PASSWORD
)
def
get_search_response
(
self
,
query
=
None
,
faceted
=
False
):
qs
=
''
if
query
:
qs
=
urllib
.
parse
.
urlencode
({
'q'
:
query
})
path
=
self
.
faceted_path
if
faceted
else
self
.
list_path
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'
}
@ddt.data
(
True
,
False
)
def
test_authentication
(
self
,
faceted
):
""" Verify the endpoint requires authentication. """
self
.
client
.
logout
()
response
=
self
.
get_search_response
(
faceted
=
faceted
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_search
(
self
):
""" Verify the view returns search results. """
self
.
assert_successful_search
(
faceted
=
False
)
def
test_faceted_search
(
self
):
""" Verify the view returns results and facets. """
course_run
,
response_data
=
self
.
assert_successful_search
(
faceted
=
True
)
# Validate the pacing facet
expected
=
{
'text'
:
course_run
.
pacing_type
,
'count'
:
1
,
}
self
.
assertDictContainsSubset
(
expected
,
response_data
[
'fields'
][
'pacing_type'
][
0
])
def
assert_successful_search
(
self
,
faceted
=
False
):
""" Asserts the search functionality returns results for a generated query. """
# Generate data that should be indexed and returned by the query
course_run
=
CourseRunFactory
(
course__title
=
'Software Testing'
)
response
=
self
.
get_search_response
(
'software'
,
faceted
=
faceted
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
# Validate the search results
expected
=
{
'count'
:
1
,
'results'
:
[
self
.
serialize_course_run
(
course_run
)
]
}
actual
=
response_data
[
'objects'
]
if
faceted
else
response_data
self
.
assertDictContainsSubset
(
expected
,
actual
)
return
course_run
,
response_data
course_discovery/apps/api/v1/urls.py
View file @
138f1434
""" API v1 URLs. """
""" API v1 URLs. """
from
rest_framework
import
routers
from
django.conf.urls
import
include
,
url
from
django.conf.urls
import
include
,
url
from
rest_framework
import
routers
from
course_discovery.apps.api.v1
import
views
from
course_discovery.apps.api.v1
import
views
partners_router
=
routers
.
SimpleRouter
()
partners_router
=
routers
.
SimpleRouter
()
partners_router
.
register
(
r'affiliate_window/catalogs'
,
views
.
AffiliateWindowViewSet
,
base_name
=
'affiliate_window'
)
partners_router
.
register
(
r'affiliate_window/catalogs'
,
views
.
AffiliateWindowViewSet
,
base_name
=
'affiliate_window'
)
partners_urls
=
partners_router
.
urls
partners_urls
=
partners_router
.
urls
...
@@ -17,5 +16,8 @@ router.register(r'catalogs', views.CatalogViewSet)
...
@@ -17,5 +16,8 @@ router.register(r'catalogs', views.CatalogViewSet)
router
.
register
(
r'courses'
,
views
.
CourseViewSet
,
base_name
=
'course'
)
router
.
register
(
r'courses'
,
views
.
CourseViewSet
,
base_name
=
'course'
)
router
.
register
(
r'course_runs'
,
views
.
CourseRunViewSet
,
base_name
=
'course_run'
)
router
.
register
(
r'course_runs'
,
views
.
CourseRunViewSet
,
base_name
=
'course_run'
)
router
.
register
(
r'management'
,
views
.
ManagementViewSet
,
base_name
=
'management'
)
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'
)
urlpatterns
+=
router
.
urls
urlpatterns
+=
router
.
urls
course_discovery/apps/api/v1/views.py
View file @
138f1434
...
@@ -11,21 +11,21 @@ from django.db.models import Q
...
@@ -11,21 +11,21 @@ from django.db.models import Q
from
django.db.models.functions
import
Lower
from
django.db.models.functions
import
Lower
from
django.http
import
HttpResponse
from
django.http
import
HttpResponse
from
django.shortcuts
import
get_object_or_404
from
django.shortcuts
import
get_object_or_404
from
drf_haystack.filters
import
HaystackFacetFilter
,
HaystackFilter
from
drf_haystack.mixins
import
FacetMixin
from
drf_haystack.viewsets
import
HaystackViewSet
from
dry_rest_permissions.generics
import
DRYPermissions
from
dry_rest_permissions.generics
import
DRYPermissions
from
edx_rest_framework_extensions.permissions
import
IsSuperuser
from
edx_rest_framework_extensions.permissions
import
IsSuperuser
from
rest_framework
import
status
,
viewsets
from
rest_framework
import
status
,
viewsets
from
rest_framework.decorators
import
detail_route
,
list_route
from
rest_framework.decorators
import
detail_route
,
list_route
from
rest_framework.exceptions
import
PermissionDenied
from
rest_framework.exceptions
import
PermissionDenied
from
rest_framework.pagination
import
PageNumberPagination
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.response
import
Response
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.filters
import
PermissionsFilter
from
course_discovery.apps.api.renderers
import
AffiliateWindowXMLRenderer
,
CourseRunCSVRenderer
from
course_discovery.apps.api.renderers
import
AffiliateWindowXMLRenderer
,
CourseRunCSVRenderer
from
course_discovery.apps.api.serializers
import
(
CatalogSerializer
,
CourseSerializer
,
CourseRunSerializer
,
ContainedCoursesSerializer
,
CourseSerializerExcludingClosedRuns
,
AffiliateWindowSerializer
,
ContainedCourseRunsSerializer
,
FlattenedCourseRunWithCourseSerializer
)
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.core.utils
import
SearchQuerySetWrapper
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.constants
import
COURSE_ID_REGEX
,
COURSE_RUN_ID_REGEX
...
@@ -43,7 +43,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -43,7 +43,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
lookup_field
=
'id'
lookup_field
=
'id'
permission_classes
=
(
DRYPermissions
,)
permission_classes
=
(
DRYPermissions
,)
queryset
=
Catalog
.
objects
.
all
()
queryset
=
Catalog
.
objects
.
all
()
serializer_class
=
CatalogSerializer
serializer_class
=
serializers
.
CatalogSerializer
@transaction.atomic
@transaction.atomic
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
def
create
(
self
,
request
,
*
args
,
**
kwargs
):
...
@@ -103,14 +103,14 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -103,14 +103,14 @@ class CatalogViewSet(viewsets.ModelViewSet):
Only courses with active course runs are returned. A course run is considered active if it is currently
Only courses with active course runs are returned. A course run is considered active if it is currently
open for enrollment, or will open in the future.
open for enrollment, or will open in the future.
---
---
serializer: CourseSerializerExcludingClosedRuns
serializer:
serializers.
CourseSerializerExcludingClosedRuns
"""
"""
catalog
=
self
.
get_object
()
catalog
=
self
.
get_object
()
queryset
=
catalog
.
courses
()
.
active
()
queryset
=
catalog
.
courses
()
.
active
()
page
=
self
.
paginate_queryset
(
queryset
)
page
=
self
.
paginate_queryset
(
queryset
)
serializer
=
CourseSerializerExcludingClosedRuns
(
page
,
many
=
True
,
context
=
{
'request'
:
request
})
serializer
=
serializers
.
CourseSerializerExcludingClosedRuns
(
page
,
many
=
True
,
context
=
{
'request'
:
request
})
return
self
.
get_paginated_response
(
serializer
.
data
)
return
self
.
get_paginated_response
(
serializer
.
data
)
@detail_route
()
@detail_route
()
...
@@ -120,7 +120,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -120,7 +120,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
A dictionary mapping course IDs to booleans, indicating course presence, will be returned.
A dictionary mapping course IDs to booleans, indicating course presence, will be returned.
---
---
serializer: ContainedCoursesSerializer
serializer:
serializers.
ContainedCoursesSerializer
parameters:
parameters:
- name: course_id
- name: course_id
description: Course IDs to check for existence in the Catalog.
description: Course IDs to check for existence in the Catalog.
...
@@ -136,7 +136,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -136,7 +136,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
courses
=
catalog
.
contains
(
course_ids
)
courses
=
catalog
.
contains
(
course_ids
)
instance
=
{
'courses'
:
courses
}
instance
=
{
'courses'
:
courses
}
serializer
=
ContainedCoursesSerializer
(
instance
)
serializer
=
serializers
.
ContainedCoursesSerializer
(
instance
)
return
Response
(
serializer
.
data
)
return
Response
(
serializer
.
data
)
@detail_route
()
@detail_route
()
...
@@ -147,7 +147,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -147,7 +147,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
Only active course runs are returned. A course run is considered active if it is currently
Only active course runs are returned. A course run is considered active if it is currently
open for enrollment, or will be open for enrollment in the future.
open for enrollment, or will be open for enrollment in the future.
---
---
serializer: FlattenedCourseRunWithCourseSerializer
serializer:
serializers.
FlattenedCourseRunWithCourseSerializer
"""
"""
catalog
=
self
.
get_object
()
catalog
=
self
.
get_object
()
courses
=
catalog
.
courses
()
.
active
()
courses
=
catalog
.
courses
()
.
active
()
...
@@ -158,7 +158,9 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -158,7 +158,9 @@ class CatalogViewSet(viewsets.ModelViewSet):
for
acr
in
active_course_runs
:
for
acr
in
active_course_runs
:
course_runs
.
append
(
acr
)
course_runs
.
append
(
acr
)
serializer
=
FlattenedCourseRunWithCourseSerializer
(
course_runs
,
many
=
True
,
context
=
{
'request'
:
request
})
serializer
=
serializers
.
FlattenedCourseRunWithCourseSerializer
(
course_runs
,
many
=
True
,
context
=
{
'request'
:
request
}
)
data
=
CourseRunCSVRenderer
()
.
render
(
serializer
.
data
)
data
=
CourseRunCSVRenderer
()
.
render
(
serializer
.
data
)
response
=
HttpResponse
(
data
,
content_type
=
'text/csv'
)
response
=
HttpResponse
(
data
,
content_type
=
'text/csv'
)
...
@@ -174,7 +176,7 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
...
@@ -174,7 +176,7 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex
=
COURSE_ID_REGEX
lookup_value_regex
=
COURSE_ID_REGEX
queryset
=
Course
.
objects
.
all
()
queryset
=
Course
.
objects
.
all
()
permission_classes
=
(
IsAuthenticated
,)
permission_classes
=
(
IsAuthenticated
,)
serializer_class
=
CourseSerializer
serializer_class
=
serializers
.
CourseSerializer
def
get_queryset
(
self
):
def
get_queryset
(
self
):
q
=
self
.
request
.
query_params
.
get
(
'q'
,
None
)
q
=
self
.
request
.
query_params
.
get
(
'q'
,
None
)
...
@@ -210,7 +212,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
...
@@ -210,7 +212,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex
=
COURSE_RUN_ID_REGEX
lookup_value_regex
=
COURSE_RUN_ID_REGEX
queryset
=
CourseRun
.
objects
.
all
()
.
order_by
(
Lower
(
'key'
))
queryset
=
CourseRun
.
objects
.
all
()
.
order_by
(
Lower
(
'key'
))
permission_classes
=
(
IsAuthenticated
,)
permission_classes
=
(
IsAuthenticated
,)
serializer_class
=
CourseRunSerializer
serializer_class
=
serializers
.
CourseRunSerializer
def
get_queryset
(
self
):
def
get_queryset
(
self
):
q
=
self
.
request
.
query_params
.
get
(
'q'
,
None
)
q
=
self
.
request
.
query_params
.
get
(
'q'
,
None
)
...
@@ -244,7 +246,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
...
@@ -244,7 +246,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
A dictionary mapping course run keys to booleans,
A dictionary mapping course run keys to booleans,
indicating course run presence, will be returned.
indicating course run presence, will be returned.
---
---
serializer: ContainedCourseRunsSerializer
serializer:
serializers.
ContainedCourseRunsSerializer
parameters:
parameters:
- name: query
- name: query
description: Elasticsearch querystring query
description: Elasticsearch querystring query
...
@@ -270,7 +272,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
...
@@ -270,7 +272,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
contains
[
course_run
.
key
]
=
True
contains
[
course_run
.
key
]
=
True
instance
=
{
'course_runs'
:
contains
}
instance
=
{
'course_runs'
:
contains
}
serializer
=
ContainedCourseRunsSerializer
(
instance
)
serializer
=
serializers
.
ContainedCourseRunsSerializer
(
instance
)
return
Response
(
serializer
.
data
)
return
Response
(
serializer
.
data
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
)
...
@@ -291,7 +293,23 @@ class ManagementViewSet(viewsets.ViewSet):
...
@@ -291,7 +293,23 @@ class ManagementViewSet(viewsets.ViewSet):
multiple: false
multiple: false
"""
"""
access_token
=
request
.
data
.
get
(
'access_token'
)
access_token
=
request
.
data
.
get
(
'access_token'
)
kwargs
=
{
'access_token'
:
access_token
}
if
access_token
else
{}
name
=
'refresh_course_metadata'
output
=
self
.
run_command
(
request
,
name
,
**
kwargs
)
return
Response
(
output
,
content_type
=
'text/plain'
)
@list_route
(
methods
=
[
'post'
])
def
update_index
(
self
,
request
):
""" Update the search index. """
name
=
'update_index'
output
=
self
.
run_command
(
request
,
name
)
return
Response
(
output
,
content_type
=
'text/plain'
)
def
run_command
(
self
,
request
,
name
,
**
kwargs
):
# Capture all output and logging
# Capture all output and logging
out
=
StringIO
()
out
=
StringIO
()
err
=
StringIO
()
err
=
StringIO
()
...
@@ -303,25 +321,20 @@ class ManagementViewSet(viewsets.ViewSet):
...
@@ -303,25 +321,20 @@ class ManagementViewSet(viewsets.ViewSet):
log_handler
.
setFormatter
(
formatter
)
log_handler
.
setFormatter
(
formatter
)
root_logger
.
addHandler
(
log_handler
)
root_logger
.
addHandler
(
log_handler
)
logger
.
info
(
'Updating course metadata per request of [
%
s]...'
,
request
.
user
.
username
)
logger
.
info
(
'Running [
%
s] per request of [
%
s]...'
,
name
,
request
.
user
.
username
)
call_command
(
name
,
settings
=
os
.
environ
[
'DJANGO_SETTINGS_MODULE'
],
stdout
=
out
,
stderr
=
err
,
**
kwargs
)
kwargs
=
{
'access_token'
:
access_token
}
if
access_token
else
{}
call_command
(
'refresh_course_metadata'
,
settings
=
os
.
environ
[
'DJANGO_SETTINGS_MODULE'
],
stdout
=
out
,
stderr
=
err
,
**
kwargs
)
# Format the output for display
# Format the output for display
output
=
'STDOUT
\n
{out}
\n\n
STDERR
\n
{err}
\n\n
LOG
\n
{log}'
.
format
(
out
=
out
.
getvalue
(),
err
=
err
.
getvalue
(),
output
=
'STDOUT
\n
{out}
\n\n
STDERR
\n
{err}
\n\n
LOG
\n
{log}'
.
format
(
out
=
out
.
getvalue
(),
err
=
err
.
getvalue
(),
log
=
log
.
getvalue
())
log
=
log
.
getvalue
())
return
output
return
Response
(
output
,
content_type
=
'text/plain'
)
class
AffiliateWindowViewSet
(
viewsets
.
ViewSet
):
class
AffiliateWindowViewSet
(
viewsets
.
ViewSet
):
""" AffiliateWindow Resource. """
""" AffiliateWindow Resource. """
permission_classes
=
(
IsAuthenticated
,)
permission_classes
=
(
IsAuthenticated
,)
renderer_classes
=
(
AffiliateWindowXMLRenderer
,)
renderer_classes
=
(
AffiliateWindowXMLRenderer
,)
serializer_class
=
AffiliateWindowSerializer
serializer_class
=
serializers
.
AffiliateWindowSerializer
def
retrieve
(
self
,
request
,
pk
=
None
):
# pylint: disable=redefined-builtin,unused-argument
def
retrieve
(
self
,
request
,
pk
=
None
):
# pylint: disable=redefined-builtin,unused-argument
"""
"""
...
@@ -345,5 +358,63 @@ class AffiliateWindowViewSet(viewsets.ViewSet):
...
@@ -345,5 +358,63 @@ class AffiliateWindowViewSet(viewsets.ViewSet):
Q
(
course_run__enrollment_end__gte
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)))
Q
(
course_run__enrollment_end__gte
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)))
)
)
serializer
=
AffiliateWindowSerializer
(
seats
,
many
=
True
)
serializer
=
serializers
.
AffiliateWindowSerializer
(
seats
,
many
=
True
)
return
Response
(
serializer
.
data
)
return
Response
(
serializer
.
data
)
class
BaseCourseHaystackViewSet
(
FacetMixin
,
HaystackViewSet
):
document_uid_field
=
'key'
facet_filter_backends
=
[
HaystackFacetFilter
,
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
# attribute, and it is more appropriate for search results than our default limit-offset pagination.
pagination_class
=
PageNumberPagination
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Search.
---
parameters:
- name: q
description: Search text
paramType: query
type: string
required: false
"""
return
super
(
BaseCourseHaystackViewSet
,
self
)
.
list
(
request
,
*
args
,
**
kwargs
)
@list_route
(
methods
=
[
"get"
],
url_path
=
"facets"
)
def
facets
(
self
,
request
):
"""
Returns faceted search results
---
parameters:
- name: q
description: Search text
paramType: query
type: string
required: false
"""
return
super
(
BaseCourseHaystackViewSet
,
self
)
.
facets
(
request
)
class
CourseSearchViewSet
(
BaseCourseHaystackViewSet
):
facet_serializer_class
=
serializers
.
CourseFacetSerializer
index_models
=
(
Course
,)
serializer_class
=
serializers
.
CourseSearchSerializer
class
CourseRunSearchViewSet
(
BaseCourseHaystackViewSet
):
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
):
""" Search all content types. """
facet_serializer_class
=
serializers
.
AggregateFacetSearchSerializer
serializer_class
=
serializers
.
AggregateSearchSerializer
course_discovery/apps/course_metadata/models.py
View file @
138f1434
...
@@ -267,6 +267,14 @@ class CourseRun(TimeStampedModel):
...
@@ -267,6 +267,14 @@ class CourseRun(TimeStampedModel):
value
=
value
or
None
value
=
value
or
None
self
.
full_description_override
=
value
self
.
full_description_override
=
value
@property
def
subjects
(
self
):
return
self
.
course
.
subjects
@property
def
organizations
(
self
):
return
self
.
course
.
organizations
@classmethod
@classmethod
def
search
(
cls
,
query
):
def
search
(
cls
,
query
):
""" Queries the search index.
""" Queries the search index.
...
...
course_discovery/apps/course_metadata/search_indexes.py
View file @
138f1434
...
@@ -4,18 +4,49 @@ from opaque_keys.edx.keys import CourseKey
...
@@ -4,18 +4,49 @@ 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
class
CourseIndex
(
indexes
.
SearchIndex
,
indexes
.
Indexable
):
class
BaseIndex
(
indexes
.
SearchIndex
):
model
=
None
text
=
indexes
.
CharField
(
document
=
True
,
use_template
=
True
)
text
=
indexes
.
CharField
(
document
=
True
,
use_template
=
True
)
content_type
=
indexes
.
CharField
(
faceted
=
True
)
def
prepare_content_type
(
self
,
obj
):
# pylint: disable=unused-argument
return
self
.
model
.
__name__
.
lower
()
def
get_model
(
self
):
return
self
.
model
def
get_updated_field
(
self
):
# pragma: no cover
return
'modified'
def
index_queryset
(
self
,
using
=
None
):
return
self
.
model
.
objects
.
all
()
class
BaseCourseIndex
(
BaseIndex
):
key
=
indexes
.
CharField
(
model_attr
=
'key'
,
stored
=
True
)
key
=
indexes
.
CharField
(
model_attr
=
'key'
,
stored
=
True
)
title
=
indexes
.
CharField
(
model_attr
=
'title'
)
title
=
indexes
.
CharField
(
model_attr
=
'title'
)
short_description
=
indexes
.
CharField
(
model_attr
=
'short_description'
,
null
=
True
)
short_description
=
indexes
.
CharField
(
model_attr
=
'short_description'
,
null
=
True
)
full_description
=
indexes
.
CharField
(
model_attr
=
'full_description'
,
null
=
True
)
full_description
=
indexes
.
CharField
(
model_attr
=
'full_description'
,
null
=
True
)
level_type
=
indexes
.
CharField
(
model_attr
=
'level_type__name'
,
null
=
True
)
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
()]
class
CourseIndex
(
BaseCourseIndex
,
indexes
.
Indexable
):
model
=
Course
level_type
=
indexes
.
CharField
(
model_attr
=
'level_type__name'
,
null
=
True
,
faceted
=
True
)
course_runs
=
indexes
.
MultiValueField
()
course_runs
=
indexes
.
MultiValueField
()
expected_learning_items
=
indexes
.
MultiValueField
()
expected_learning_items
=
indexes
.
MultiValueField
()
organizations
=
indexes
.
MultiValueField
()
prerequisites
=
indexes
.
MultiValueField
()
prerequisites
=
indexes
.
MultiValueField
(
faceted
=
True
)
subjects
=
indexes
.
MultiValueField
()
def
prepare_course_runs
(
self
,
obj
):
def
prepare_course_runs
(
self
,
obj
):
return
[
course_run
.
key
for
course_run
in
obj
.
course_runs
.
all
()]
return
[
course_run
.
key
for
course_run
in
obj
.
course_runs
.
all
()]
...
@@ -23,46 +54,30 @@ class CourseIndex(indexes.SearchIndex, indexes.Indexable):
...
@@ -23,46 +54,30 @@ class CourseIndex(indexes.SearchIndex, indexes.Indexable):
def
prepare_expected_learning_items
(
self
,
obj
):
def
prepare_expected_learning_items
(
self
,
obj
):
return
[
item
.
value
for
item
in
obj
.
expected_learning_items
.
all
()]
return
[
item
.
value
for
item
in
obj
.
expected_learning_items
.
all
()]
def
prepare_organizations
(
self
,
obj
):
return
[
'{key}: {name}'
.
format
(
key
=
organization
.
key
,
name
=
organization
.
name
)
for
organization
in
obj
.
organizations
.
all
()]
def
prepare_prerequisites
(
self
,
obj
):
def
prepare_prerequisites
(
self
,
obj
):
return
[
prerequisite
.
name
for
prerequisite
in
obj
.
prerequisites
.
all
()]
return
[
prerequisite
.
name
for
prerequisite
in
obj
.
prerequisites
.
all
()]
def
prepare_subjects
(
self
,
obj
):
return
[
subject
.
name
for
subject
in
obj
.
subjects
.
all
()]
def
get_model
(
self
):
class
CourseRunIndex
(
BaseCourseIndex
,
indexes
.
Indexable
):
return
Course
model
=
CourseRun
def
index_queryset
(
self
,
using
=
None
):
return
self
.
get_model
()
.
objects
.
all
()
def
get_updated_field
(
self
):
# pragma: no cover
return
'modified'
class
CourseRunIndex
(
indexes
.
SearchIndex
,
indexes
.
Indexable
):
text
=
indexes
.
CharField
(
document
=
True
,
use_template
=
True
)
course_key
=
indexes
.
CharField
(
model_attr
=
'course__key'
,
stored
=
True
)
course_key
=
indexes
.
CharField
(
model_attr
=
'course__key'
,
stored
=
True
)
key
=
indexes
.
CharField
(
model_attr
=
'key'
,
stored
=
True
)
org
=
indexes
.
CharField
()
org
=
indexes
.
CharField
()
number
=
indexes
.
CharField
()
number
=
indexes
.
CharField
()
title
=
indexes
.
CharField
(
model_attr
=
'title_override'
,
null
=
True
)
start
=
indexes
.
DateTimeField
(
model_attr
=
'start'
,
null
=
True
,
faceted
=
True
)
start
=
indexes
.
DateTimeField
(
model_attr
=
'start'
,
null
=
True
)
end
=
indexes
.
DateTimeField
(
model_attr
=
'end'
,
null
=
True
)
end
=
indexes
.
DateTimeField
(
model_attr
=
'end'
,
null
=
True
)
enrollment_start
=
indexes
.
DateTimeField
(
model_attr
=
'enrollment_start'
,
null
=
True
)
enrollment_start
=
indexes
.
DateTimeField
(
model_attr
=
'enrollment_start'
,
null
=
True
)
enrollment_end
=
indexes
.
DateTimeField
(
model_attr
=
'enrollment_end'
,
null
=
True
)
enrollment_end
=
indexes
.
DateTimeField
(
model_attr
=
'enrollment_end'
,
null
=
True
)
announcement
=
indexes
.
DateTimeField
(
model_attr
=
'announcement'
,
null
=
True
)
announcement
=
indexes
.
DateTimeField
(
model_attr
=
'announcement'
,
null
=
True
)
min_effort
=
indexes
.
IntegerField
(
model_attr
=
'min_effort'
,
null
=
True
)
min_effort
=
indexes
.
IntegerField
(
model_attr
=
'min_effort'
,
null
=
True
)
max_effort
=
indexes
.
IntegerField
(
model_attr
=
'max_effort'
,
null
=
True
)
max_effort
=
indexes
.
IntegerField
(
model_attr
=
'max_effort'
,
null
=
True
)
language
=
indexes
.
CharField
(
null
=
True
)
language
=
indexes
.
CharField
(
null
=
True
,
faceted
=
True
)
transcript_languages
=
indexes
.
MultiValueField
()
transcript_languages
=
indexes
.
MultiValueField
(
faceted
=
True
)
pacing_type
=
indexes
.
CharField
(
model_attr
=
'pacing_type'
,
null
=
True
)
pacing_type
=
indexes
.
CharField
(
model_attr
=
'pacing_type'
,
null
=
True
,
faceted
=
True
)
marketing_url
=
indexes
.
CharField
(
model_attr
=
'marketing_url'
,
null
=
True
)
def
_prepare_language
(
self
,
language
):
def
_prepare_language
(
self
,
language
):
return
'{code}: {name}'
.
format
(
code
=
language
.
code
,
name
=
language
.
name
)
return
language
.
name
def
prepare_language
(
self
,
obj
):
def
prepare_language
(
self
,
obj
):
if
obj
.
language
:
if
obj
.
language
:
...
@@ -79,9 +94,3 @@ class CourseRunIndex(indexes.SearchIndex, indexes.Indexable):
...
@@ -79,9 +94,3 @@ class CourseRunIndex(indexes.SearchIndex, indexes.Indexable):
def
prepare_transcript_languages
(
self
,
obj
):
def
prepare_transcript_languages
(
self
,
obj
):
return
[
self
.
_prepare_language
(
language
)
for
language
in
obj
.
transcript_languages
.
all
()]
return
[
self
.
_prepare_language
(
language
)
for
language
in
obj
.
transcript_languages
.
all
()]
def
get_model
(
self
):
return
CourseRun
def
get_updated_field
(
self
):
# pragma: no cover
return
'modified'
course_discovery/apps/course_metadata/views.py
View file @
138f1434
...
@@ -2,4 +2,8 @@ from django.views.generic import TemplateView
...
@@ -2,4 +2,8 @@ from django.views.generic import TemplateView
class
QueryPreviewView
(
TemplateView
):
class
QueryPreviewView
(
TemplateView
):
template_name
=
'catalogs/preview.html'
template_name
=
'demo/query_preview.html'
class
SearchDemoView
(
TemplateView
):
template_name
=
'demo/search.html'
course_discovery/templates/
catalogs/
preview.html
→
course_discovery/templates/
demo/query_
preview.html
View file @
138f1434
File moved
course_discovery/templates/demo/search.html
0 → 100644
View file @
138f1434
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF-8"
>
<title>
Search Demo
</title>
</head>
<body>
<h2>
Search
</h2>
<form
method=
"get"
action=
"."
>
<table>
{{ form.as_table }}
<tr>
<td>
</td>
<td>
<input
type=
"submit"
value=
"Search"
>
</td>
</tr>
</table>
{% if query %}
<h3>
Results
</h3>
{% for result in page.object_list %}
<p>
<a
href=
"{{ result.object.get_absolute_url }}"
>
{{ result.object.title }}
</a>
</p>
{% empty %}
<p>
No results found.
</p>
{% endfor %}
{% if page.has_previous or page.has_next %}
<div>
{% if page.has_previous %}
<a
href=
"?q={{ query }}&page={{ page.previous_page_number }}"
>
{% endif %}
«
Previous
{% if page.has_previous %}
</a>
{% endif %}
|
{% if page.has_next %}
<a
href=
"?q={{ query }}&page={{ page.next_page_number }}"
>
{% endif %}
Next
»
{% if page.has_next %}
</a>
{% endif %}
</div>
{% endif %}
{% else %}
{# Show some example queries to run, maybe query syntax, something else? #}
{% endif %}
</form>
</body>
</html>
\ No newline at end of file
course_discovery/templates/search/indexes/course_metadata/courserun_text.txt
View file @
138f1434
...
@@ -3,3 +3,7 @@
...
@@ -3,3 +3,7 @@
{{ object.short_description|default:'' }}
{{ object.short_description|default:'' }}
{{ object.full_description|default:'' }}
{{ object.full_description|default:'' }}
{{ object.pacing_type|default:'' }}
{{ object.pacing_type|default:'' }}
{% for language in object.transcript_languages.all %}
{{ language }}
{% endfor %}
requirements/base.txt
View file @
138f1434
...
@@ -11,6 +11,7 @@ djangorestframework-csv==1.4.1
...
@@ -11,6 +11,7 @@ djangorestframework-csv==1.4.1
djangorestframework-jwt==1.8.0
djangorestframework-jwt==1.8.0
djangorestframework-xml==1.3.0
djangorestframework-xml==1.3.0
django-rest-swagger[reST]==0.3.7
django-rest-swagger[reST]==0.3.7
drf-haystack==1.6.0rc1
dry-rest-permissions==0.1.6
dry-rest-permissions==0.1.6
edx-auth-backends==0.5.0
edx-auth-backends==0.5.0
edx-ccx-keys==0.2.0
edx-ccx-keys==0.2.0
...
...
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