Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-analytics-data-api
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
edx-analytics-data-api
Commits
66914fa0
Commit
66914fa0
authored
Oct 07, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Updated CSV filename
File names are now compatible with opaque keys.
parent
af18880a
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
162 additions
and
75 deletions
+162
-75
analytics_data_api/v0/tests/test_views.py
+139
-70
analytics_data_api/v0/views/courses.py
+22
-5
requirements/base.txt
+1
-0
No files found.
analytics_data_api/v0/tests/test_views.py
View file @
66914fa0
# coding=utf-8
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# for subsequent versions if there are breaking changes introduced in those versions.
# for subsequent versions if there are breaking changes introduced in those versions.
...
@@ -5,11 +6,13 @@ import StringIO
...
@@ -5,11 +6,13 @@ import StringIO
import
csv
import
csv
import
datetime
import
datetime
from
itertools
import
groupby
from
itertools
import
groupby
import
urllib
from
django.conf
import
settings
from
django.conf
import
settings
from
django_dynamic_fixture
import
G
from
django_dynamic_fixture
import
G
from
iso3166
import
countries
from
iso3166
import
countries
import
pytz
import
pytz
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0.constants
import
UNKNOWN_COUNTRY
,
UNKNOWN_COUNTRY_CODE
from
analytics_data_api.v0.constants
import
UNKNOWN_COUNTRY
,
UNKNOWN_COUNTRY_CODE
...
@@ -19,12 +22,29 @@ from analytics_data_api.v0.tests.utils import flatten
...
@@ -19,12 +22,29 @@ from analytics_data_api.v0.tests.utils import flatten
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
DEMO_COURSE_ID
=
u'course-v1:edX+DemoX+Demo_2014'
class
DemoCourseMixin
(
object
):
course_key
=
None
course_id
=
None
def
setUp
(
self
):
self
.
course_id
=
DEMO_COURSE_ID
self
.
course_key
=
CourseKey
.
from_string
(
self
.
course_id
)
super
(
DemoCourseMixin
,
self
)
.
setUp
()
# pylint: disable=no-member
# pylint: disable=no-member
class
CourseViewTestCaseMixin
(
object
):
class
CourseViewTestCaseMixin
(
DemoCourseMixin
):
model
=
None
model
=
None
api_root_path
=
'/api/v0/'
api_root_path
=
'/api/v0/'
path
=
None
path
=
None
order_by
=
[]
order_by
=
[]
csv_filename_slug
=
None
def
generate_data
(
self
,
course_id
=
None
):
raise
NotImplementedError
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
"""
"""
...
@@ -35,7 +55,7 @@ class CourseViewTestCaseMixin(object):
...
@@ -35,7 +55,7 @@ class CourseViewTestCaseMixin(object):
"""
"""
raise
NotImplementedError
raise
NotImplementedError
def
get_latest_data
(
self
):
def
get_latest_data
(
self
,
course_id
=
None
):
"""
"""
Return the latest row/rows that would be returned if a user made a call
Return the latest row/rows that would be returned if a user made a call
to the endpoint with no date filtering.
to the endpoint with no date filtering.
...
@@ -44,34 +64,37 @@ class CourseViewTestCaseMixin(object):
...
@@ -44,34 +64,37 @@ class CourseViewTestCaseMixin(object):
"""
"""
raise
NotImplementedError
raise
NotImplementedError
def
get_csv_filename
(
self
):
return
u'edX-DemoX-Demo_2014--{0}.csv'
.
format
(
self
.
csv_filename_slug
)
def
test_get_not_found
(
self
):
def
test_get_not_found
(
self
):
""" Requests made against non-existent courses should return a 404 """
""" Requests made against non-existent courses should return a 404 """
course_id
=
'edX/DemoX/Non_Existent_Course'
course_id
=
u
'edX/DemoX/Non_Existent_Course'
response
=
self
.
authenticated_get
(
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
course_id
,
self
.
path
))
response
=
self
.
authenticated_get
(
u
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_get
(
self
):
def
test_get
(
self
):
""" Verify the endpoint returns an HTTP 200 status and the correct data. """
""" Verify the endpoint returns an HTTP 200 status and the correct data. """
# Validate the basic response status
# Validate the basic response status
response
=
self
.
authenticated_get
(
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
))
response
=
self
.
authenticated_get
(
u
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
# Validate the data is correct and sorted chronologically
# Validate the data is correct and sorted chronologically
expected
=
self
.
format_as_response
(
*
self
.
get_latest_data
())
expected
=
self
.
format_as_response
(
*
self
.
get_latest_data
())
self
.
assertEquals
(
response
.
data
,
expected
)
self
.
assertEquals
(
response
.
data
,
expected
)
def
test_get_csv
(
self
):
def
assertCSVIsValid
(
self
,
course_id
,
filename
):
""" Verify the endpoint returns data that has been properly converted to CSV. """
path
=
u'{0}courses/{1}{2}'
.
format
(
self
.
api_root_path
,
course_id
,
self
.
path
)
path
=
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
)
csv_content_type
=
'text/csv'
csv_content_type
=
'text/csv'
response
=
self
.
authenticated_get
(
path
,
HTTP_ACCEPT
=
csv_content_type
)
response
=
self
.
authenticated_get
(
path
,
HTTP_ACCEPT
=
csv_content_type
)
# Validate the basic response status
and content cod
e
# Validate the basic response status
, content type, and filenam
e
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
[
'Content-Type'
]
.
split
(
';'
)[
0
],
csv_content_type
)
self
.
assertEquals
(
response
[
'Content-Type'
]
.
split
(
';'
)[
0
],
csv_content_type
)
self
.
assertEquals
(
response
[
'Content-Disposition'
],
u'attachment; filename={}'
.
format
(
filename
))
# Validate the actual data
# Validate the actual data
data
=
self
.
format_as_response
(
*
self
.
get_latest_data
())
data
=
self
.
format_as_response
(
*
self
.
get_latest_data
(
course_id
=
course_id
))
data
=
map
(
flatten
,
data
)
data
=
map
(
flatten
,
data
)
# The CSV renderer sorts the headers alphabetically
# The CSV renderer sorts the headers alphabetically
...
@@ -82,9 +105,21 @@ class CourseViewTestCaseMixin(object):
...
@@ -82,9 +105,21 @@ class CourseViewTestCaseMixin(object):
writer
=
csv
.
DictWriter
(
expected
,
fieldnames
)
writer
=
csv
.
DictWriter
(
expected
,
fieldnames
)
writer
.
writeheader
()
writer
.
writeheader
()
writer
.
writerows
(
data
)
writer
.
writerows
(
data
)
self
.
assertEqual
(
response
.
content
,
expected
.
getvalue
())
self
.
assertEqual
(
response
.
content
,
expected
.
getvalue
())
def
test_get_csv
(
self
):
""" Verify the endpoint returns data that has been properly converted to CSV. """
self
.
assertCSVIsValid
(
self
.
course_id
,
self
.
get_csv_filename
())
def
test_get_csv_with_deprecated_key
(
self
):
"""
Verify the endpoint returns data that has been properly converted to CSV even if the course ID is deprecated.
"""
course_id
=
u'edX/DemoX/Demo_Course'
self
.
generate_data
(
course_id
)
filename
=
u'{0}--{1}.csv'
.
format
(
u'edX-DemoX-Demo_Course'
,
self
.
csv_filename_slug
)
self
.
assertCSVIsValid
(
course_id
,
filename
)
def
test_get_with_intervals
(
self
):
def
test_get_with_intervals
(
self
):
""" Verify the endpoint returns multiple data points when supplied with an interval of dates. """
""" Verify the endpoint returns multiple data points when supplied with an interval of dates. """
raise
NotImplementedError
raise
NotImplementedError
...
@@ -115,46 +150,50 @@ class CourseViewTestCaseMixin(object):
...
@@ -115,46 +150,50 @@ class CourseViewTestCaseMixin(object):
# pylint: disable=abstract-method
# pylint: disable=abstract-method
class
CourseEnrollmentViewTestCaseMixin
(
CourseViewTestCaseMixin
):
class
CourseEnrollmentViewTestCaseMixin
(
CourseViewTestCaseMixin
):
date
=
None
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseEnrollmentViewTestCaseMixin
,
self
)
.
setUp
()
super
(
CourseEnrollmentViewTestCaseMixin
,
self
)
.
setUp
()
self
.
course_id
=
'edX/DemoX/Demo_Course'
self
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
self
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
def
get_latest_data
(
self
):
def
get_latest_data
(
self
,
course_id
=
None
):
return
self
.
model
.
objects
.
filter
(
date
=
self
.
date
)
.
order_by
(
'date'
,
*
self
.
order_by
)
course_id
=
course_id
or
self
.
course_id
return
self
.
model
.
objects
.
filter
(
course_id
=
course_id
,
date
=
self
.
date
)
.
order_by
(
'date'
,
*
self
.
order_by
)
def
test_get_with_intervals
(
self
):
def
test_get_with_intervals
(
self
):
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
date
,
self
.
date
+
datetime
.
timedelta
(
days
=
1
))
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
date
,
self
.
date
+
datetime
.
timedelta
(
days
=
1
))
class
CourseActivityLastWeekTest
(
TestCaseWithAuthentication
):
class
CourseActivityLastWeekTest
(
DemoCourseMixin
,
TestCaseWithAuthentication
):
# pylint: disable=line-too-long
def
generate_data
(
self
,
course_id
=
None
):
def
setUp
(
self
):
course_id
=
course_id
or
self
.
course_id
super
(
CourseActivityLastWeekTest
,
self
)
.
setUp
()
self
.
course_id
=
'edX/DemoX/Demo_Course'
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
interval_end
=
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
interval_end
=
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
# G(models.CourseActivityWeekly, course_id=
self.
course_id, interval_start=interval_start,
# G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start,
#
interval_end=interval_end,
# interval_end=interval_end,
#
activity_type='POSTED_FORUM', count=100)
# activity_type='POSTED_FORUM', count=100)
G
(
models
.
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivityWeekly
,
course_id
=
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
interval_end
=
interval_end
,
activity_type
=
'ATTEMPTED_PROBLEM'
,
count
=
200
)
activity_type
=
'ATTEMPTED_PROBLEM'
,
count
=
200
)
G
(
models
.
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivityWeekly
,
course_id
=
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
interval_end
=
interval_end
,
activity_type
=
'ACTIVE'
,
count
=
300
)
activity_type
=
'ACTIVE'
,
count
=
300
)
G
(
models
.
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivityWeekly
,
course_id
=
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
interval_end
=
interval_end
,
activity_type
=
'PLAYED_VIDEO'
,
count
=
400
)
activity_type
=
'PLAYED_VIDEO'
,
count
=
400
)
def
setUp
(
self
):
super
(
CourseActivityLastWeekTest
,
self
)
.
setUp
()
self
.
generate_data
()
def
test_activity
(
self
):
def
test_activity
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
))
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
())
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
())
def
assertValidActivityResponse
(
self
,
activity_type
,
count
):
def
assertValidActivityResponse
(
self
,
activity_type
,
count
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
self
.
course_id
,
activity_type
))
self
.
course_id
,
activity_type
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
count
))
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
count
))
...
@@ -162,7 +201,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
...
@@ -162,7 +201,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
@staticmethod
@staticmethod
def
get_activity_record
(
**
kwargs
):
def
get_activity_record
(
**
kwargs
):
default
=
{
default
=
{
'course_id'
:
'edX/DemoX/Demo_Course'
,
'course_id'
:
DEMO_COURSE_ID
,
'interval_start'
:
datetime
.
datetime
(
2014
,
1
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_start'
:
datetime
.
datetime
(
2014
,
1
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_end'
:
datetime
.
datetime
(
2014
,
1
,
8
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_end'
:
datetime
.
datetime
(
2014
,
1
,
8
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'activity_type'
:
'any'
,
'activity_type'
:
'any'
,
...
@@ -173,11 +212,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
...
@@ -173,11 +212,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
return
default
return
default
def
test_activity_auth
(
self
):
def
test_activity_auth
(
self
):
response
=
self
.
client
.
get
(
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
),
follow
=
True
)
response
=
self
.
client
.
get
(
u
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
),
follow
=
True
)
self
.
assertEquals
(
response
.
status_code
,
401
)
self
.
assertEquals
(
response
.
status_code
,
401
)
def
test_url_encoded_course_id
(
self
):
def
test_url_encoded_course_id
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/edX
%2
FDemoX
%2
FDemo_Course/recent_activity'
)
url_encoded_course_id
=
urllib
.
quote_plus
(
self
.
course_id
)
response
=
self
.
authenticated_get
(
u'/api/v0/courses/{}/recent_activity'
.
format
(
url_encoded_course_id
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
())
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
())
...
@@ -190,21 +230,21 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
...
@@ -190,21 +230,21 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
def
test_unknown_activity
(
self
):
def
test_unknown_activity
(
self
):
activity_type
=
'missing_activity_type'
activity_type
=
'missing_activity_type'
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
self
.
course_id
,
activity_type
))
self
.
course_id
,
activity_type
))
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_unknown_course_id
(
self
):
def
test_unknown_course_id
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity'
.
format
(
'foo'
))
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity'
.
format
(
'foo'
))
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_missing_course_id
(
self
):
def
test_missing_course_id
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/recent_activity'
)
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/recent_activity'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_label_parameter
(
self
):
def
test_label_parameter
(
self
):
activity_type
=
'played_video'
activity_type
=
'played_video'
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity?label={1}'
.
format
(
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity?label={1}'
.
format
(
self
.
course_id
,
activity_type
))
self
.
course_id
,
activity_type
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
400
))
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
400
))
...
@@ -214,17 +254,22 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te
...
@@ -214,17 +254,22 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te
path
=
'/enrollment/birth_year'
path
=
'/enrollment/birth_year'
model
=
models
.
CourseEnrollmentByBirthYear
model
=
models
.
CourseEnrollmentByBirthYear
order_by
=
[
'birth_year'
]
order_by
=
[
'birth_year'
]
csv_filename_slug
=
u'enrollment-age'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
birth_year
=
1986
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1986
)
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseEnrollmentByBirthYearViewTests
,
self
)
.
setUp
()
super
(
CourseEnrollmentByBirthYearViewTests
,
self
)
.
setUp
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
birth_year
=
1956
)
self
.
generate_data
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
birth_year
=
1986
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1986
)
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
return
[
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'birth_year'
:
ce
.
birth_year
,
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
'birth_year'
:
ce
.
birth_year
,
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
def
test_get
(
self
):
def
test_get
(
self
):
...
@@ -239,19 +284,23 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
...
@@ -239,19 +284,23 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
path
=
'/enrollment/education/'
path
=
'/enrollment/education/'
model
=
models
.
CourseEnrollmentByEducation
model
=
models
.
CourseEnrollmentByEducation
order_by
=
[
'education_level'
]
order_by
=
[
'education_level'
]
csv_filename_slug
=
u'enrollment-education'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el1
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el2
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
education_level
=
self
.
el2
)
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseEnrollmentByEducationViewTests
,
self
)
.
setUp
()
super
(
CourseEnrollmentByEducationViewTests
,
self
)
.
setUp
()
self
.
el1
=
G
(
models
.
EducationLevel
,
name
=
'Doctorate'
,
short_name
=
'doctorate'
)
self
.
el1
=
G
(
models
.
EducationLevel
,
name
=
'Doctorate'
,
short_name
=
'doctorate'
)
self
.
el2
=
G
(
models
.
EducationLevel
,
name
=
'Top Secret'
,
short_name
=
'top_secret'
)
self
.
el2
=
G
(
models
.
EducationLevel
,
name
=
'Top Secret'
,
short_name
=
'top_secret'
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el1
)
self
.
generate_data
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el2
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
education_level
=
self
.
el2
)
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
return
[
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'education_level'
:
{
'name'
:
ce
.
education_level
.
name
,
'short_name'
:
ce
.
education_level
.
short_name
},
'education_level'
:
{
'name'
:
ce
.
education_level
.
name
,
'short_name'
:
ce
.
education_level
.
short_name
},
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
ce
in
args
]
...
@@ -261,16 +310,21 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
...
@@ -261,16 +310,21 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
path
=
'/enrollment/gender/'
path
=
'/enrollment/gender/'
model
=
models
.
CourseEnrollmentByGender
model
=
models
.
CourseEnrollmentByGender
order_by
=
[
'gender'
]
order_by
=
[
'gender'
]
csv_filename_slug
=
u'enrollment-gender'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
gender
=
'm'
,
date
=
self
.
date
,
count
=
34
)
G
(
self
.
model
,
course_id
=
course_id
,
gender
=
'f'
,
date
=
self
.
date
,
count
=
45
)
G
(
self
.
model
,
course_id
=
course_id
,
gender
=
'f'
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
count
=
45
)
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseEnrollmentByGenderViewTests
,
self
)
.
setUp
()
super
(
CourseEnrollmentByGenderViewTests
,
self
)
.
setUp
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
gender
=
'm'
,
date
=
self
.
date
,
count
=
34
)
self
.
generate_data
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
gender
=
'f'
,
date
=
self
.
date
,
count
=
45
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
gender
=
'f'
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
count
=
45
)
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
return
[
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'gender'
:
ce
.
gender
,
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
'gender'
:
ce
.
gender
,
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
...
@@ -308,15 +362,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
...
@@ -308,15 +362,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
class
CourseEnrollmentViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
class
CourseEnrollmentViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
model
=
models
.
CourseEnrollmentDaily
model
=
models
.
CourseEnrollmentDaily
path
=
'/enrollment'
path
=
'/enrollment'
csv_filename_slug
=
u'enrollment'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
count
=
203
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
5
),
count
=
203
)
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseEnrollmentViewTests
,
self
)
.
setUp
()
super
(
CourseEnrollmentViewTests
,
self
)
.
setUp
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
count
=
203
)
self
.
generate_data
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
5
),
count
=
203
)
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
return
[
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
for
ce
in
args
]
...
@@ -324,6 +383,7 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
...
@@ -324,6 +383,7 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
class
CourseEnrollmentByLocationViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
class
CourseEnrollmentByLocationViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/enrollment/location/'
path
=
'/enrollment/location/'
model
=
models
.
CourseEnrollmentByCountry
model
=
models
.
CourseEnrollmentByCountry
csv_filename_slug
=
u'enrollment-location'
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
unknown
=
{
'course_id'
:
None
,
'count'
:
0
,
'date'
:
None
,
unknown
=
{
'course_id'
:
None
,
'count'
:
0
,
'date'
:
None
,
...
@@ -341,26 +401,29 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
...
@@ -341,26 +401,29 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
response
=
[
unknown
]
response
=
[
unknown
]
response
+=
[
response
+=
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'country'
:
{
'alpha2'
:
ce
.
country
.
alpha2
,
'alpha3'
:
ce
.
country
.
alpha3
,
'name'
:
ce
.
country
.
name
},
'country'
:
{
'alpha2'
:
ce
.
country
.
alpha2
,
'alpha3'
:
ce
.
country
.
alpha3
,
'name'
:
ce
.
country
.
name
},
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
args
]
return
response
return
response
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'US'
,
count
=
455
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'CA'
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'IN'
,
count
=
12
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
29
))
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
''
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'A1'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'A2'
,
count
=
2
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'AP'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'EU'
,
count
=
4
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'O1'
,
count
=
7
,
date
=
self
.
date
)
def
setUp
(
self
):
def
setUp
(
self
):
super
(
CourseEnrollmentByLocationViewTests
,
self
)
.
setUp
()
super
(
CourseEnrollmentByLocationViewTests
,
self
)
.
setUp
()
self
.
country
=
countries
.
get
(
'US'
)
self
.
country
=
countries
.
get
(
'US'
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'US'
,
count
=
455
,
date
=
self
.
date
)
self
.
generate_data
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'CA'
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'IN'
,
count
=
12
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
29
))
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
''
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'A1'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'A2'
,
count
=
2
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'AP'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'EU'
,
count
=
4
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'O1'
,
count
=
7
,
date
=
self
.
date
)
class
CourseActivityWeeklyViewTests
(
CourseViewTestCaseMixin
,
TestCaseWithAuthentication
):
class
CourseActivityWeeklyViewTests
(
CourseViewTestCaseMixin
,
TestCaseWithAuthentication
):
...
@@ -369,23 +432,29 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
...
@@ -369,23 +432,29 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
model
=
CourseActivityWeekly
model
=
CourseActivityWeekly
# activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO', 'POSTED_FORUM']
# activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO', 'POSTED_FORUM']
activity_types
=
[
'ACTIVE'
,
'ATTEMPTED_PROBLEM'
,
'PLAYED_VIDEO'
]
activity_types
=
[
'ACTIVE'
,
'ATTEMPTED_PROBLEM'
,
'PLAYED_VIDEO'
]
csv_filename_slug
=
u'engagement-activity'
def
setUp
(
self
):
def
generate_data
(
self
,
course_id
=
None
):
super
(
CourseActivityWeeklyViewTests
,
self
)
.
setUp
()
course_id
=
course_id
or
self
.
course_id
self
.
course_id
=
'edX/DemoX/Demo_Course'
self
.
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
self
.
interval_end
=
self
.
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
for
activity_type
in
self
.
activity_types
:
for
activity_type
in
self
.
activity_types
:
G
(
CourseActivityWeekly
,
G
(
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
course_id
=
course_id
,
interval_start
=
self
.
interval_start
,
interval_start
=
self
.
interval_start
,
interval_end
=
self
.
interval_end
,
interval_end
=
self
.
interval_end
,
activity_type
=
activity_type
,
activity_type
=
activity_type
,
count
=
100
)
count
=
100
)
def
get_latest_data
(
self
):
def
setUp
(
self
):
return
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
,
interval_end
=
self
.
interval_end
)
super
(
CourseActivityWeeklyViewTests
,
self
)
.
setUp
()
self
.
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
self
.
interval_end
=
self
.
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
self
.
generate_data
()
def
get_latest_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
return
self
.
model
.
objects
.
filter
(
course_id
=
course_id
,
interval_end
=
self
.
interval_end
)
def
format_as_response
(
self
,
*
args
):
def
format_as_response
(
self
,
*
args
):
response
=
[]
response
=
[]
...
...
analytics_data_api/v0/views/courses.py
View file @
66914fa0
...
@@ -8,6 +8,7 @@ from django.db.models import Max
...
@@ -8,6 +8,7 @@ from django.db.models import Max
from
django.http
import
Http404
from
django.http
import
Http404
from
django.utils.timezone
import
make_aware
,
utc
from
django.utils.timezone
import
make_aware
,
utc
from
rest_framework
import
generics
from
rest_framework
import
generics
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.v0
import
models
,
serializers
from
analytics_data_api.v0
import
models
,
serializers
...
@@ -15,8 +16,11 @@ from analytics_data_api.v0 import models, serializers
...
@@ -15,8 +16,11 @@ from analytics_data_api.v0 import models, serializers
class
BaseCourseView
(
generics
.
ListAPIView
):
class
BaseCourseView
(
generics
.
ListAPIView
):
start_date
=
None
start_date
=
None
end_date
=
None
end_date
=
None
course_id
=
None
slug
=
None
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
start_date
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
start_date
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
end_date
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
end_date
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
timezone
=
utc
timezone
=
utc
...
@@ -44,12 +48,21 @@ class BaseCourseView(generics.ListAPIView):
...
@@ -44,12 +48,21 @@ class BaseCourseView(generics.ListAPIView):
raise
NotImplementedError
raise
NotImplementedError
def
get_queryset
(
self
):
def
get_queryset
(
self
):
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
self
.
verify_course_exists_or_404
(
self
.
course_id
)
self
.
verify_course_exists_or_404
(
course_id
)
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
course_id
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
return
queryset
return
queryset
def
get_csv_filename
(
self
):
course_key
=
CourseKey
.
from_string
(
self
.
course_id
)
course_id
=
u'-'
.
join
([
course_key
.
org
,
course_key
.
course
,
course_key
.
run
])
return
u'{0}--{1}.csv'
.
format
(
course_id
,
self
.
slug
)
def
finalize_response
(
self
,
request
,
response
,
*
args
,
**
kwargs
):
if
request
.
META
.
get
(
'HTTP_ACCEPT'
)
==
u'text/csv'
:
response
[
'Content-Disposition'
]
=
u'attachment; filename={}'
.
format
(
self
.
get_csv_filename
())
return
super
(
BaseCourseView
,
self
)
.
finalize_response
(
request
,
response
,
*
args
,
**
kwargs
)
# pylint: disable=line-too-long
# pylint: disable=line-too-long
class
CourseActivityWeeklyView
(
BaseCourseView
):
class
CourseActivityWeeklyView
(
BaseCourseView
):
...
@@ -80,6 +93,7 @@ class CourseActivityWeeklyView(BaseCourseView):
...
@@ -80,6 +93,7 @@ class CourseActivityWeeklyView(BaseCourseView):
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
slug
=
u'engagement-activity'
model
=
models
.
CourseActivityWeekly
model
=
models
.
CourseActivityWeekly
serializer_class
=
serializers
.
CourseActivityWeeklySerializer
serializer_class
=
serializers
.
CourseActivityWeeklySerializer
...
@@ -244,6 +258,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView):
...
@@ -244,6 +258,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView):
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
slug
=
u'enrollment-age'
serializer_class
=
serializers
.
CourseEnrollmentByBirthYearSerializer
serializer_class
=
serializers
.
CourseEnrollmentByBirthYearSerializer
model
=
models
.
CourseEnrollmentByBirthYear
model
=
models
.
CourseEnrollmentByBirthYear
...
@@ -263,6 +278,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView):
...
@@ -263,6 +278,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
slug
=
u'enrollment-education'
serializer_class
=
serializers
.
CourseEnrollmentByEducationSerializer
serializer_class
=
serializers
.
CourseEnrollmentByEducationSerializer
model
=
models
.
CourseEnrollmentByEducation
model
=
models
.
CourseEnrollmentByEducation
...
@@ -287,6 +303,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
...
@@ -287,6 +303,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
slug
=
u'enrollment-gender'
serializer_class
=
serializers
.
CourseEnrollmentByGenderSerializer
serializer_class
=
serializers
.
CourseEnrollmentByGenderSerializer
model
=
models
.
CourseEnrollmentByGender
model
=
models
.
CourseEnrollmentByGender
...
@@ -304,7 +321,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView):
...
@@ -304,7 +321,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
slug
=
u'enrollment'
serializer_class
=
serializers
.
CourseEnrollmentDailySerializer
serializer_class
=
serializers
.
CourseEnrollmentDailySerializer
model
=
models
.
CourseEnrollmentDaily
model
=
models
.
CourseEnrollmentDaily
...
@@ -329,7 +346,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
...
@@ -329,7 +346,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
slug
=
u'enrollment-location'
serializer_class
=
serializers
.
CourseEnrollmentByCountrySerializer
serializer_class
=
serializers
.
CourseEnrollmentByCountrySerializer
model
=
models
.
CourseEnrollmentByCountry
model
=
models
.
CourseEnrollmentByCountry
...
...
requirements/base.txt
View file @
66914fa0
...
@@ -7,3 +7,4 @@ ipython==2.1.0 # BSD
...
@@ -7,3 +7,4 @@ ipython==2.1.0 # BSD
django-rest-swagger==0.1.14 # BSD
django-rest-swagger==0.1.14 # BSD
djangorestframework-csv==1.3.3 # BSD
djangorestframework-csv==1.3.3 # BSD
iso3166==0.1 # MIT
iso3166==0.1 # MIT
-e git+https://github.com/edx/opaque-keys.git@d45d0bd8d64c69531be69178b9505b5d38806ce0#egg=opaque-keys
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