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
1f771669
Commit
1f771669
authored
Mar 29, 2016
by
Bill DeRusha
Committed by
Clinton Blackburn
Mar 29, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add Course Metadata models
parent
d1d54641
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
147 additions
and
302 deletions
+147
-302
course_discovery/apps/api/pagination.py
+0
-25
course_discovery/apps/api/serializers.py
+8
-4
course_discovery/apps/api/tests/test_serializers.py
+3
-4
course_discovery/apps/api/v1/tests/test_views.py
+10
-53
course_discovery/apps/api/v1/views.py
+6
-41
course_discovery/apps/catalogs/models.py
+28
-26
course_discovery/apps/catalogs/tests/test_models.py
+9
-3
course_discovery/apps/core/tests/mixins.py
+1
-1
course_discovery/apps/core/tests/test_throttles.py
+1
-1
course_discovery/apps/core/utils.py
+1
-3
course_discovery/apps/course_metadata/admin.py
+33
-0
course_discovery/apps/course_metadata/apps.py
+1
-1
course_discovery/apps/course_metadata/config.py
+0
-26
course_discovery/apps/course_metadata/constants.py
+2
-2
course_discovery/apps/course_metadata/exceptions.py
+0
-3
course_discovery/apps/course_metadata/management/__init__.py
+0
-0
course_discovery/apps/course_metadata/management/commands/__init__.py
+0
-0
course_discovery/apps/course_metadata/management/commands/install_es_indexes.py
+1
-1
course_discovery/apps/course_metadata/management/commands/refresh_all_courses.py
+0
-40
course_discovery/apps/course_metadata/migrations/0001_initial.py
+0
-0
course_discovery/apps/course_metadata/models.py
+0
-0
course_discovery/apps/course_metadata/tests/factories.py
+37
-20
course_discovery/apps/course_metadata/tests/test_models.py
+0
-0
course_discovery/apps/course_metadata/tests/test_refresh_all_courses.py
+0
-44
course_discovery/settings/base.py
+3
-0
course_discovery/settings/utils.py
+1
-4
requirements/base.txt
+2
-0
No files found.
course_discovery/apps/api/pagination.py
deleted
100644 → 0
View file @
d1d54641
from
rest_framework.pagination
import
LimitOffsetPagination
class
ElasticsearchLimitOffsetPagination
(
LimitOffsetPagination
):
def
paginate_queryset
(
self
,
queryset
,
request
,
view
=
None
):
"""
Convert a paginated Elasticsearch response to a response suitable for DRF.
Args:
queryset (dict): Elasticsearch response
request (Request): HTTP request
Returns:
List of data.
"""
# pylint: disable=attribute-defined-outside-init
self
.
limit
=
self
.
get_limit
(
request
)
self
.
offset
=
self
.
get_offset
(
request
)
self
.
count
=
queryset
[
'total'
]
self
.
request
=
request
if
self
.
count
>
self
.
limit
and
self
.
template
is
not
None
:
self
.
display_page_controls
=
True
return
queryset
[
'results'
]
course_discovery/apps/api/serializers.py
View file @
1f771669
...
@@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy as _
...
@@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy as _
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
Course
class
CatalogSerializer
(
serializers
.
ModelSerializer
):
class
CatalogSerializer
(
serializers
.
ModelSerializer
):
...
@@ -12,10 +13,13 @@ class CatalogSerializer(serializers.ModelSerializer):
...
@@ -12,10 +13,13 @@ class CatalogSerializer(serializers.ModelSerializer):
fields
=
(
'id'
,
'name'
,
'query'
,
'url'
,)
fields
=
(
'id'
,
'name'
,
'query'
,
'url'
,)
class
CourseSerializer
(
serializers
.
Serializer
):
# pylint: disable=abstract-method
class
CourseSerializer
(
serializers
.
ModelSerializer
):
id
=
serializers
.
CharField
(
help_text
=
_
(
'Course ID'
))
key
=
serializers
.
CharField
()
name
=
serializers
.
CharField
(
help_text
=
_
(
'Course name'
))
title
=
serializers
.
CharField
()
url
=
serializers
.
HyperlinkedIdentityField
(
view_name
=
'api:v1:course-detail'
,
lookup_field
=
'id'
)
class
Meta
(
object
):
model
=
Course
fields
=
(
'key'
,
'title'
,)
class
ContainedCoursesSerializer
(
serializers
.
Serializer
):
# pylint: disable=abstract-method
class
ContainedCoursesSerializer
(
serializers
.
Serializer
):
# pylint: disable=abstract-method
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
1f771669
...
@@ -25,14 +25,13 @@ class CatalogSerializerTests(TestCase):
...
@@ -25,14 +25,13 @@ class CatalogSerializerTests(TestCase):
class
CourseSerializerTests
(
TestCase
):
class
CourseSerializerTests
(
TestCase
):
def
test_data
(
self
):
def
test_data
(
self
):
course
=
CourseFactory
()
course
=
CourseFactory
()
path
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'
id'
:
course
.
id
})
path
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'
key'
:
course
.
key
})
request
=
RequestFactory
()
.
get
(
path
)
request
=
RequestFactory
()
.
get
(
path
)
serializer
=
CourseSerializer
(
course
,
context
=
{
'request'
:
request
})
serializer
=
CourseSerializer
(
course
,
context
=
{
'request'
:
request
})
expected
=
{
expected
=
{
'id'
:
course
.
id
,
'key'
:
course
.
key
,
'name'
:
course
.
name
,
'title'
:
course
.
title
,
'url'
:
request
.
build_absolute_uri
(),
}
}
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
...
...
course_discovery/apps/api/v1/tests/test_views.py
View file @
1f771669
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
import
json
import
json
import
urllib
import
urllib
from
time
import
time
from
time
import
time
from
unittest
import
skip
import
ddt
import
ddt
import
jwt
import
jwt
...
@@ -70,21 +71,8 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
...
@@ -70,21 +71,8 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
super
(
CatalogViewSetTests
,
self
)
.
setUp
()
super
(
CatalogViewSetTests
,
self
)
.
setUp
()
self
.
user
=
UserFactory
(
is_staff
=
True
,
is_superuser
=
True
)
self
.
user
=
UserFactory
(
is_staff
=
True
,
is_superuser
=
True
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
USER_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
USER_PASSWORD
)
query
=
{
self
.
catalog
=
CatalogFactory
(
query
=
'title:abc*'
)
'query'
:
{
self
.
course
=
CourseFactory
(
key
=
'a/b/c'
,
title
=
'ABC Test Course'
)
'bool'
:
{
'must'
:
[
{
'wildcard'
:
{
'course.name'
:
'abc*'
}
}
]
}
}
}
self
.
catalog
=
CatalogFactory
(
query
=
json
.
dumps
(
query
))
self
.
course
=
CourseFactory
(
id
=
'a/b/c'
,
name
=
'ABC Test Course'
)
self
.
refresh_index
()
self
.
refresh_index
()
def
generate_jwt_token_header
(
self
,
user
):
def
generate_jwt_token_header
(
self
,
user
):
...
@@ -153,6 +141,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
...
@@ -153,6 +141,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
self
.
mock_user_info_response
(
self
.
user
)
self
.
mock_user_info_response
(
self
.
user
)
self
.
assert_catalog_created
(
HTTP_AUTHORIZATION
=
self
.
generate_oauth2_token_header
(
self
.
user
))
self
.
assert_catalog_created
(
HTTP_AUTHORIZATION
=
self
.
generate_oauth2_token_header
(
self
.
user
))
@skip
(
'Re-enable once we switch to Haystack'
)
def
test_courses
(
self
):
def
test_courses
(
self
):
""" Verify the endpoint returns the list of courses contained in the catalog. """
""" Verify the endpoint returns the list of courses contained in the catalog. """
url
=
reverse
(
'api:v1:catalog-courses'
,
kwargs
=
{
'id'
:
self
.
catalog
.
id
})
url
=
reverse
(
'api:v1:catalog-courses'
,
kwargs
=
{
'id'
:
self
.
catalog
.
id
})
...
@@ -162,15 +151,16 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
...
@@ -162,15 +151,16 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
[
'results'
],
self
.
serialize_course
(
courses
,
many
=
True
))
self
.
assertListEqual
(
response
.
data
[
'results'
],
self
.
serialize_course
(
courses
,
many
=
True
))
@skip
(
'Re-enable once we switch to Haystack'
)
def
test_contains
(
self
):
def
test_contains
(
self
):
""" Verify the endpoint returns a filtered list of courses contained in the catalog. """
""" Verify the endpoint returns a filtered list of courses contained in the catalog. """
course_
id
=
self
.
course
.
id
course_
key
=
self
.
course
.
key
qs
=
urllib
.
parse
.
urlencode
({
'course_id'
:
course_
id
})
qs
=
urllib
.
parse
.
urlencode
({
'course_id'
:
course_
key
})
url
=
'{}?{}'
.
format
(
reverse
(
'api:v1:catalog-contains'
,
kwargs
=
{
'id'
:
self
.
catalog
.
id
}),
qs
)
url
=
'{}?{}'
.
format
(
reverse
(
'api:v1:catalog-contains'
,
kwargs
=
{
'id'
:
self
.
catalog
.
id
}),
qs
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
data
,
{
'courses'
:
{
course_
id
:
True
}})
self
.
assertEqual
(
response
.
data
,
{
'courses'
:
{
course_
key
:
True
}})
def
test_get
(
self
):
def
test_get
(
self
):
""" Verify the endpoint returns the details for a single catalog. """
""" Verify the endpoint returns the details for a single catalog. """
...
@@ -242,10 +232,9 @@ class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin
...
@@ -242,10 +232,9 @@ class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin
def
test_list
(
self
,
format
):
def
test_list
(
self
,
format
):
""" Verify the endpoint returns a list of all courses. """
""" Verify the endpoint returns a list of all courses. """
courses
=
CourseFactory
.
create_batch
(
10
)
courses
=
CourseFactory
.
create_batch
(
10
)
courses
.
sort
(
key
=
lambda
course
:
course
.
id
.
lower
())
courses
.
sort
(
key
=
lambda
course
:
course
.
key
.
lower
())
url
=
reverse
(
'api:v1:course-list'
)
url
=
reverse
(
'api:v1:course-list'
)
limit
=
3
limit
=
3
self
.
refresh_index
()
response
=
self
.
client
.
get
(
url
,
{
'format'
:
format
,
'limit'
:
limit
})
response
=
self
.
client
.
get
(
url
,
{
'format'
:
format
,
'limit'
:
limit
})
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
@@ -253,38 +242,6 @@ class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin
...
@@ -253,38 +242,6 @@ class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin
response
.
render
()
response
.
render
()
def
test_list_query
(
self
):
""" Verify the endpoint returns a filtered list of courses. """
# Create courses that should NOT match our query
CourseFactory
.
create_batch
(
3
)
# Create courses that SHOULD match our query
name
=
'query test'
courses
=
[
CourseFactory
(
name
=
name
),
CourseFactory
(
name
=
name
)]
courses
.
sort
(
key
=
lambda
course
:
course
.
id
.
lower
())
self
.
refresh_index
()
query
=
{
"query"
:
{
"bool"
:
{
"must"
:
[
{
"term"
:
{
"course.name"
:
name
}
}
]
}
}
}
qs
=
urllib
.
parse
.
urlencode
({
'q'
:
json
.
dumps
(
query
)})
url
=
'{}?{}'
.
format
(
reverse
(
'api:v1:course-list'
),
qs
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
data
[
'count'
],
len
(
courses
))
self
.
assertListEqual
(
response
.
data
[
'results'
],
self
.
serialize_course
(
courses
,
many
=
True
))
def
test_retrieve
(
self
):
def
test_retrieve
(
self
):
""" Verify the endpoint returns a single course. """
""" Verify the endpoint returns a single course. """
self
.
assert_retrieve_success
()
self
.
assert_retrieve_success
()
...
@@ -292,7 +249,7 @@ class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin
...
@@ -292,7 +249,7 @@ class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin
def
assert_retrieve_success
(
self
,
**
headers
):
def
assert_retrieve_success
(
self
,
**
headers
):
""" Asserts the endpoint returns details for a single course. """
""" Asserts the endpoint returns details for a single course. """
course
=
CourseFactory
()
course
=
CourseFactory
()
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'
id'
:
course
.
id
})
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'
key'
:
course
.
key
})
response
=
self
.
client
.
get
(
url
,
format
=
'json'
,
**
headers
)
response
=
self
.
client
.
get
(
url
,
format
=
'json'
,
**
headers
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
data
,
self
.
serialize_course
(
course
))
self
.
assertEqual
(
response
.
data
,
self
.
serialize_course
(
course
))
...
...
course_discovery/apps/api/v1/views.py
View file @
1f771669
import
json
import
logging
import
logging
from
django.db.models.functions
import
Lower
from
rest_framework
import
viewsets
from
rest_framework
import
viewsets
from
rest_framework.decorators
import
detail_route
from
rest_framework.decorators
import
detail_route
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.pagination
import
ElasticsearchLimitOffsetPagination
from
course_discovery.apps.api.serializers
import
CatalogSerializer
,
CourseSerializer
,
ContainedCoursesSerializer
from
course_discovery.apps.api.serializers
import
CatalogSerializer
,
CourseSerializer
,
ContainedCoursesSerializer
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.catalogs.models
import
Catalog
from
course_discovery.apps.course_metadata.constants
import
COURSE_ID_REGEX
from
course_discovery.apps.course_metadata.constants
import
COURSE_ID_REGEX
...
@@ -91,49 +90,15 @@ class CatalogViewSet(viewsets.ModelViewSet):
...
@@ -91,49 +90,15 @@ class CatalogViewSet(viewsets.ModelViewSet):
class
CourseViewSet
(
viewsets
.
ReadOnlyModelViewSet
):
class
CourseViewSet
(
viewsets
.
ReadOnlyModelViewSet
):
""" Course resource. """
""" Course resource. """
lookup_field
=
'
id
'
lookup_field
=
'
key
'
lookup_value_regex
=
COURSE_ID_REGEX
lookup_value_regex
=
COURSE_ID_REGEX
permission_classes
=
(
IsAuthenticated
,)
permission_classes
=
(
IsAuthenticated
,)
serializer_class
=
CourseSerializer
serializer_class
=
CourseSerializer
pagination_class
=
ElasticsearchLimitOffsetPagination
queryset
=
Course
.
objects
.
all
()
.
order_by
(
Lower
(
'key'
))
def
get_object
(
self
):
""" Return a single course. """
return
Course
.
get
(
self
.
kwargs
[
self
.
lookup_url_kwarg
or
self
.
lookup_field
])
def
get_queryset
(
self
):
# Note (CCB): This is solely here to appease DRF. It is not actually used.
return
[]
def
get_data
(
self
,
limit
,
offset
):
""" Return all courses. """
query
=
self
.
request
.
GET
.
get
(
'q'
,
None
)
if
query
:
query
=
json
.
loads
(
query
)
return
Course
.
search
(
query
,
limit
=
limit
,
offset
=
offset
)
else
:
return
Course
.
all
(
limit
=
limit
,
offset
=
offset
)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
List all courses.
---
parameters:
- name: q
description: Query to filter the courses
required: false
type: string
paramType: query
multiple: false
"""
limit
=
self
.
paginator
.
get_limit
(
self
.
request
)
offset
=
self
.
paginator
.
get_offset
(
self
.
request
)
data
=
self
.
get_data
(
limit
,
offset
)
page
=
self
.
paginate_queryset
(
data
)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
self
.
get_serializer
(
page
,
many
=
True
)
""" List all courses. """
return
s
elf
.
get_paginated_response
(
serializer
.
data
)
return
s
uper
(
CourseViewSet
,
self
)
.
list
(
request
,
*
args
,
**
kwargs
)
def
retrieve
(
self
,
request
,
*
args
,
**
kwargs
):
def
retrieve
(
self
,
request
,
*
args
,
**
kwargs
):
""" Retrieve details for a course. """
""" Retrieve details for a course. """
...
...
course_discovery/apps/catalogs/models.py
View file @
1f771669
...
@@ -4,8 +4,6 @@ from django.db import models
...
@@ -4,8 +4,6 @@ from django.db import models
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.translation
import
ugettext_lazy
as
_
from
django_extensions.db.models
import
TimeStampedModel
from
django_extensions.db.models
import
TimeStampedModel
from
course_discovery.apps.course_metadata.models
import
Course
class
Catalog
(
TimeStampedModel
):
class
Catalog
(
TimeStampedModel
):
name
=
models
.
CharField
(
max_length
=
255
,
null
=
False
,
blank
=
False
,
help_text
=
_
(
'Catalog name'
))
name
=
models
.
CharField
(
max_length
=
255
,
null
=
False
,
blank
=
False
,
help_text
=
_
(
'Catalog name'
))
...
@@ -25,7 +23,8 @@ class Catalog(TimeStampedModel):
...
@@ -25,7 +23,8 @@ class Catalog(TimeStampedModel):
Course[]
Course[]
"""
"""
return
Course
.
search
(
self
.
query_as_dict
)[
'results'
]
# TODO: Course.search no longer exists. Figure out what goes here.
# return Course.search(self.query_as_dict)['results']
def
contains
(
self
,
course_ids
):
# pylint: disable=unused-argument
def
contains
(
self
,
course_ids
):
# pylint: disable=unused-argument
""" Determines if the given courses are contained in this catalog.
""" Determines if the given courses are contained in this catalog.
...
@@ -37,26 +36,29 @@ class Catalog(TimeStampedModel):
...
@@ -37,26 +36,29 @@ class Catalog(TimeStampedModel):
dict: Mapping of course IDs to booleans indicating if course is
dict: Mapping of course IDs to booleans indicating if course is
contained in this catalog.
contained in this catalog.
"""
"""
query
=
self
.
query_as_dict
[
'query'
]
# query = self.query_as_dict['query']
# Create a filtered query that includes that uses the catalog's query against a
# # Create a filtered query that includes that uses the catalog's query against a
# collection of courses filtered using the passed in course IDs.
# # collection of courses filtered using the passed in course IDs.
filtered_query
=
{
# filtered_query = {
"query"
:
{
# "query": {
"filtered"
:
{
# "filtered": {
"query"
:
query
,
# "query": query,
"filter"
:
{
# "filter": {
"ids"
:
{
# "ids": {
"values"
:
course_ids
# "values": course_ids
}
# }
}
# }
}
# }
}
# }
}
# }
contains
=
{
course_id
:
False
for
course_id
in
course_ids
}
# contains = {course_id: False for course_id in course_ids}
courses
=
Course
.
search
(
filtered_query
)[
'results'
]
for
course
in
courses
:
# TODO: Course.search no longer exists. Figure out what goes here.
contains
[
course
.
id
]
=
True
# courses = Course.search(filtered_query)['results']
# for course in courses:
return
contains
# contains[course.id] = True
# return contains
pass
course_discovery/apps/catalogs/tests/test_models.py
View file @
1f771669
import
json
import
json
from
unittest
import
skip
from
django.test
import
TestCase
from
django.test
import
TestCase
...
@@ -18,7 +19,7 @@ class CatalogTests(ElasticsearchTestMixin, TestCase):
...
@@ -18,7 +19,7 @@ class CatalogTests(ElasticsearchTestMixin, TestCase):
'must'
:
[
'must'
:
[
{
{
'wildcard'
:
{
'wildcard'
:
{
'course.
nam
e'
:
'abc*'
'course.
titl
e'
:
'abc*'
}
}
}
}
]
]
...
@@ -26,7 +27,7 @@ class CatalogTests(ElasticsearchTestMixin, TestCase):
...
@@ -26,7 +27,7 @@ class CatalogTests(ElasticsearchTestMixin, TestCase):
}
}
}
}
self
.
catalog
=
factories
.
CatalogFactory
(
query
=
json
.
dumps
(
query
))
self
.
catalog
=
factories
.
CatalogFactory
(
query
=
json
.
dumps
(
query
))
self
.
course
=
CourseFactory
(
id
=
'a/b/c'
,
nam
e
=
'ABCs of Ͳҽʂէìղց'
)
self
.
course
=
CourseFactory
(
key
=
'a/b/c'
,
titl
e
=
'ABCs of Ͳҽʂէìղց'
)
self
.
refresh_index
()
self
.
refresh_index
()
def
test_unicode
(
self
):
def
test_unicode
(
self
):
...
@@ -38,11 +39,16 @@ class CatalogTests(ElasticsearchTestMixin, TestCase):
...
@@ -38,11 +39,16 @@ class CatalogTests(ElasticsearchTestMixin, TestCase):
expected
=
'Catalog #{id}: {name}'
.
format
(
id
=
self
.
catalog
.
id
,
name
=
name
)
expected
=
'Catalog #{id}: {name}'
.
format
(
id
=
self
.
catalog
.
id
,
name
=
name
)
self
.
assertEqual
(
str
(
self
.
catalog
),
expected
)
self
.
assertEqual
(
str
(
self
.
catalog
),
expected
)
@skip
(
'Skip until searching in ES is resolved'
)
def
test_courses
(
self
):
def
test_courses
(
self
):
""" Verify the method returns a list of courses contained in the catalog. """
""" Verify the method returns a list of courses contained in the catalog. """
self
.
assertEqual
(
self
.
catalog
.
courses
(),
[
self
.
course
])
self
.
assertEqual
(
self
.
catalog
.
courses
(),
[
self
.
course
])
@skip
(
'Skip until searching in ES is resolved'
)
def
test_contains
(
self
):
def
test_contains
(
self
):
""" Verify the method returns a mapping of course IDs to booleans. """
""" Verify the method returns a mapping of course IDs to booleans. """
other_id
=
'd/e/f'
other_id
=
'd/e/f'
self
.
assertDictEqual
(
self
.
catalog
.
contains
([
self
.
course
.
id
,
other_id
]),
{
self
.
course
.
id
:
True
,
other_id
:
False
})
self
.
assertDictEqual
(
self
.
catalog
.
contains
([
self
.
course
.
key
,
other_id
]),
{
self
.
course
.
key
:
True
,
other_id
:
False
}
)
course_discovery/apps/core/tests/mixins.py
View file @
1f771669
...
@@ -3,7 +3,7 @@ import logging
...
@@ -3,7 +3,7 @@ import logging
from
django.conf
import
settings
from
django.conf
import
settings
from
elasticsearch
import
Elasticsearch
from
elasticsearch
import
Elasticsearch
from
course_discovery.apps.co
urse_metadata
.utils
import
ElasticsearchUtils
from
course_discovery.apps.co
re
.utils
import
ElasticsearchUtils
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/core/tests/test_throttles.py
View file @
1f771669
...
@@ -15,7 +15,7 @@ class RateLimitingTest(APITestCase):
...
@@ -15,7 +15,7 @@ class RateLimitingTest(APITestCase):
def
setUp
(
self
):
def
setUp
(
self
):
super
(
RateLimitingTest
,
self
)
.
setUp
()
super
(
RateLimitingTest
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'api:v1:c
ourse
-list'
)
self
.
url
=
reverse
(
'api:v1:c
atalog
-list'
)
self
.
user
=
UserFactory
()
self
.
user
=
UserFactory
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
USER_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
USER_PASSWORD
)
...
...
course_discovery/apps/co
urse_metadata
/utils.py
→
course_discovery/apps/co
re
/utils.py
View file @
1f771669
import
datetime
import
datetime
import
logging
import
logging
from
course_discovery.apps.course_metadata.config
import
COURSES_INDEX_CONFIG
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
...
@@ -18,7 +16,7 @@ class ElasticsearchUtils(object):
...
@@ -18,7 +16,7 @@ class ElasticsearchUtils(object):
# Create an index with a unique (timestamped) name
# Create an index with a unique (timestamped) name
timestamp
=
datetime
.
datetime
.
utcnow
()
.
strftime
(
"
%
Y
%
m
%
d
%
H
%
M
%
S"
)
timestamp
=
datetime
.
datetime
.
utcnow
()
.
strftime
(
"
%
Y
%
m
%
d
%
H
%
M
%
S"
)
index
=
'{alias}_{timestamp}'
.
format
(
alias
=
alias
,
timestamp
=
timestamp
)
index
=
'{alias}_{timestamp}'
.
format
(
alias
=
alias
,
timestamp
=
timestamp
)
es
.
indices
.
create
(
index
=
index
,
body
=
COURSES_INDEX_CONFIG
)
es
.
indices
.
create
(
index
=
index
)
logger
.
info
(
'...index [
%
s] created.'
,
index
)
logger
.
info
(
'...index [
%
s] created.'
,
index
)
# Point the alias to the new index
# Point the alias to the new index
...
...
course_discovery/apps/course_metadata/admin.py
0 → 100644
View file @
1f771669
from
django.contrib
import
admin
from
course_discovery.apps.course_metadata.models
import
(
Seat
,
Image
,
Video
,
LevelType
,
Subject
,
Prerequisite
,
ExpectedLearningItem
,
Course
,
CourseRun
,
Organization
,
Person
,
CourseOrganization
,
SyllabusItem
)
class
CourseOrganizationInline
(
admin
.
TabularInline
):
model
=
CourseOrganization
extra
=
1
class
SeatInline
(
admin
.
TabularInline
):
model
=
Seat
extra
=
1
@admin.register
(
Course
)
class
CourseAdmin
(
admin
.
ModelAdmin
):
inlines
=
(
CourseOrganizationInline
,)
@admin.register
(
CourseRun
)
class
CourseRunAdmin
(
admin
.
ModelAdmin
):
inlines
=
(
SeatInline
,)
# Register all models using basic ModelAdmin classes
models
=
(
Image
,
Video
,
LevelType
,
Subject
,
Prerequisite
,
ExpectedLearningItem
,
Organization
,
Person
,
SyllabusItem
)
for
model
in
models
:
admin
.
site
.
register
(
model
)
course_discovery/apps/course_metadata/apps.py
View file @
1f771669
...
@@ -2,5 +2,5 @@ from django.apps import AppConfig
...
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class
CourseMetadataConfig
(
AppConfig
):
class
CourseMetadataConfig
(
AppConfig
):
name
=
'course_metadata'
name
=
'course_
discovery.apps.course_
metadata'
verbose_name
=
'Course Metadata'
verbose_name
=
'Course Metadata'
course_discovery/apps/course_metadata/config.py
deleted
100644 → 0
View file @
d1d54641
COURSES_INDEX_CONFIG
=
{
'settings'
:
{
'analysis'
:
{
'analyzer'
:
{
'lowercase_keyword'
:
{
'tokenizer'
:
'keyword'
,
'filter'
:
[
'lowercase'
]
}
}
}
},
'mappings'
:
{
'course'
:
{
'properties'
:
{
'id'
:
{
'type'
:
'string'
,
'analyzer'
:
'lowercase_keyword'
},
'name'
:
{
'type'
:
'string'
,
'analyzer'
:
'lowercase_keyword'
}
}
}
}
}
course_discovery/apps/course_metadata/constants.py
View file @
1f771669
COURSE_ID_REGEX
=
r'[^/+]+(/|\+)[^/+]+
(/|\+)[^/]+
'
COURSE_ID_REGEX
=
r'[^/+]+(/|\+)[^/+]+'
COURSE_
ID_PATTERN
=
r'(?P<id>{})'
.
format
(
COURSE_ID_REGEX
)
COURSE_
RUN_ID_REGEX
=
r'[^/+]+(/|\+)[^/+]+(/|\+)[^/]+'
course_discovery/apps/course_metadata/exceptions.py
deleted
100644 → 0
View file @
d1d54641
class
CourseNotFoundError
(
Exception
):
""" The specified course was not found in the data store. """
pass
course_discovery/apps/course_metadata/management/__init__.py
deleted
100644 → 0
View file @
d1d54641
course_discovery/apps/course_metadata/management/commands/__init__.py
deleted
100644 → 0
View file @
d1d54641
course_discovery/apps/course_metadata/management/commands/install_es_indexes.py
View file @
1f771669
...
@@ -4,7 +4,7 @@ from django.conf import settings
...
@@ -4,7 +4,7 @@ from django.conf import settings
from
django.core.management
import
BaseCommand
from
django.core.management
import
BaseCommand
from
elasticsearch
import
Elasticsearch
from
elasticsearch
import
Elasticsearch
from
course_discovery.apps.co
urse_metadata
.utils
import
ElasticsearchUtils
from
course_discovery.apps.co
re
.utils
import
ElasticsearchUtils
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/course_metadata/management/commands/refresh_all_courses.py
deleted
100644 → 0
View file @
d1d54641
import
logging
from
django.conf
import
settings
from
django.core.management
import
BaseCommand
from
edx_rest_api_client.client
import
EdxRestApiClient
from
course_discovery.apps.course_metadata.models
import
Course
logger
=
logging
.
getLogger
(
__name__
)
class
Command
(
BaseCommand
):
help
=
'Refresh course data from external sources.'
def
add_arguments
(
self
,
parser
):
parser
.
add_argument
(
'--access_token'
,
action
=
'store'
,
dest
=
'access_token'
,
default
=
None
,
help
=
'OAuth2 access token used to authenticate API calls.'
)
def
handle
(
self
,
*
args
,
**
options
):
access_token
=
options
.
get
(
'access_token'
)
if
not
access_token
:
logger
.
info
(
'No access token provided. Retrieving access token using client_credential flow...'
)
try
:
access_token
,
__
=
EdxRestApiClient
.
get_oauth_access_token
(
'{root}/access_token'
.
format
(
root
=
settings
.
SOCIAL_AUTH_EDX_OIDC_URL_ROOT
),
settings
.
SOCIAL_AUTH_EDX_OIDC_KEY
,
settings
.
SOCIAL_AUTH_EDX_OIDC_SECRET
)
except
Exception
:
logger
.
exception
(
'No access token provided or acquired through client_credential flow.'
)
raise
Course
.
refresh_all
(
access_token
=
access_token
)
course_discovery/apps/course_metadata/migrations/0001_initial.py
0 → 100644
View file @
1f771669
This diff is collapsed.
Click to expand it.
course_discovery/apps/course_metadata/models.py
View file @
1f771669
This diff is collapsed.
Click to expand it.
course_discovery/apps/course_metadata/tests/factories.py
View file @
1f771669
import
factory
import
factory
from
factory.fuzzy
import
FuzzyText
from
factory.fuzzy
import
FuzzyText
from
course_discovery.apps.course_metadata.models
import
Course
from
course_discovery.apps.course_metadata.models
import
Course
,
CourseRun
,
Organization
,
Person
class
CourseFactory
(
factory
.
Factory
):
class
CourseFactory
(
factory
.
DjangoModelFactory
):
class
Meta
(
object
):
key
=
FuzzyText
(
prefix
=
'course-id/'
)
title
=
FuzzyText
(
prefix
=
"Test çօմɾʂҽ "
)
short_description
=
FuzzyText
(
prefix
=
"Test çօմɾʂҽ short description"
)
full_description
=
FuzzyText
(
prefix
=
"Test çօմɾʂҽ FULL description"
)
class
Meta
:
model
=
Course
model
=
Course
exclude
=
(
'name'
,)
id
=
FuzzyText
(
prefix
=
'course-id/'
,
suffix
=
'/fake'
)
class
CourseRunFactory
(
factory
.
DjangoModelFactory
):
name
=
FuzzyText
(
prefix
=
"էҽʂէ çօմɾʂҽ "
)
key
=
FuzzyText
(
prefix
=
'course-run-id/'
,
suffix
=
'/fake'
)
course
=
factory
.
SubFactory
(
CourseFactory
)
@factory.lazy_attribute
title_override
=
None
def
body
(
self
):
short_description_override
=
None
return
{
full_description_override
=
None
'id'
:
self
.
id
,
'name'
:
self
.
name
class
Meta
:
}
model
=
CourseRun
@classmethod
def
_create
(
cls
,
model_class
,
*
args
,
**
kwargs
):
class
OrganizationFactory
(
factory
.
DjangoModelFactory
):
obj
=
model_class
(
*
args
,
**
kwargs
)
key
=
FuzzyText
(
prefix
=
'Org.fake/'
)
obj
.
save
()
name
=
FuzzyText
()
return
obj
class
Meta
:
model
=
Organization
class
PersonFactory
(
factory
.
DjangoModelFactory
):
key
=
FuzzyText
(
prefix
=
'Person.fake/'
)
name
=
FuzzyText
()
title
=
FuzzyText
()
bio
=
FuzzyText
()
class
Meta
:
model
=
Person
course_discovery/apps/course_metadata/tests/test_models.py
View file @
1f771669
This diff is collapsed.
Click to expand it.
course_discovery/apps/course_metadata/tests/test_refresh_all_courses.py
deleted
100644 → 0
View file @
d1d54641
""" Tests for Refresh All Courses management command. """
from
django.core.management
import
call_command
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
edx_rest_api_client.client
import
EdxRestApiClient
from
mock
import
patch
from
course_discovery.apps.course_metadata.models
import
Course
@override_settings
(
SOCIAL_AUTH_EDX_OIDC_URL_ROOT
=
"http://auth-url.com/oauth2"
,
SOCIAL_AUTH_EDX_OIDC_KEY
=
"client_id"
,
SOCIAL_AUTH_EDX_OIDC_SECRET
=
"client_secret"
)
class
RefreshAllCoursesCommandTests
(
TestCase
):
""" Tests for refresh_all_courses management command. """
cmd
=
'refresh_all_courses'
def
test_call_with_access_token
(
self
):
""" Verify the management command calls Course.refresh_all() with access token. """
access_token
=
'secret'
with
patch
.
object
(
Course
,
'refresh_all'
)
as
mock_refresh
:
call_command
(
self
.
cmd
,
access_token
=
access_token
)
mock_refresh
.
assert_called_once_with
(
access_token
=
access_token
)
def
test_call_with_client_credentials
(
self
):
""" Verify the management command calls Course.refresh_all() with client credentials. """
access_token
=
'secret'
with
patch
.
object
(
EdxRestApiClient
,
'get_oauth_access_token'
)
as
mock_access_token
:
mock_access_token
.
return_value
=
(
access_token
,
None
)
with
patch
.
object
(
Course
,
'refresh_all'
)
as
mock_refresh
:
call_command
(
self
.
cmd
)
mock_refresh
.
assert_called_once_with
(
access_token
=
access_token
)
def
test_call_with_client_credentials_error
(
self
):
""" Verify the command requires an access token to complete. """
with
patch
.
object
(
EdxRestApiClient
,
'get_oauth_access_token'
)
as
mock_access_token
:
mock_access_token
.
side_effect
=
Exception
()
with
self
.
assertRaises
(
Exception
):
call_command
(
self
.
cmd
)
course_discovery/settings/base.py
View file @
1f771669
...
@@ -33,6 +33,8 @@ THIRD_PARTY_APPS = (
...
@@ -33,6 +33,8 @@ THIRD_PARTY_APPS = (
'rest_framework_swagger'
,
'rest_framework_swagger'
,
'social.apps.django_app.default'
,
'social.apps.django_app.default'
,
'waffle'
,
'waffle'
,
'sortedm2m'
,
'simple_history'
,
)
)
PROJECT_APPS
=
(
PROJECT_APPS
=
(
...
@@ -57,6 +59,7 @@ MIDDLEWARE_CLASSES = (
...
@@ -57,6 +59,7 @@ MIDDLEWARE_CLASSES = (
'django.middleware.clickjacking.XFrameOptionsMiddleware'
,
'django.middleware.clickjacking.XFrameOptionsMiddleware'
,
'social.apps.django_app.middleware.SocialAuthExceptionMiddleware'
,
'social.apps.django_app.middleware.SocialAuthExceptionMiddleware'
,
'waffle.middleware.WaffleMiddleware'
,
'waffle.middleware.WaffleMiddleware'
,
'simple_history.middleware.HistoryRequestMiddleware'
,
)
)
ROOT_URLCONF
=
'course_discovery.urls'
ROOT_URLCONF
=
'course_discovery.urls'
...
...
course_discovery/settings/utils.py
View file @
1f771669
from
os
import
environ
,
path
from
os
import
environ
import
sys
from
logging.handlers
import
SysLogHandler
from
django.core.exceptions
import
ImproperlyConfigured
from
django.core.exceptions
import
ImproperlyConfigured
...
@@ -12,4 +10,3 @@ def get_env_setting(setting):
...
@@ -12,4 +10,3 @@ def get_env_setting(setting):
except
KeyError
:
except
KeyError
:
error_msg
=
"Set the [{}] env variable!"
.
format
(
setting
)
error_msg
=
"Set the [{}] env variable!"
.
format
(
setting
)
raise
ImproperlyConfigured
(
error_msg
)
raise
ImproperlyConfigured
(
error_msg
)
requirements/base.txt
View file @
1f771669
django==1.8.7
django==1.8.7
django-extensions==1.5.9
django-extensions==1.5.9
django-simple-history==1.8.1
django-sortedm2m==1.1.1
django-waffle==0.11
django-waffle==0.11
djangorestframework==3.3.1
djangorestframework==3.3.1
djangorestframework-jwt==1.7.2
djangorestframework-jwt==1.7.2
...
...
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