Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
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-platform
Commits
481a4b7e
Commit
481a4b7e
authored
Oct 10, 2014
by
Zia Fazal
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
initial changes
added tests added users_completed added users_not_started
parent
99f90c76
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
310 additions
and
3 deletions
+310
-3
lms/djangoapps/api_manager/courses/tests.py
+141
-1
lms/djangoapps/api_manager/courses/urls.py
+1
-0
lms/djangoapps/api_manager/courses/views.py
+76
-2
lms/djangoapps/api_manager/utils.py
+92
-0
No files found.
lms/djangoapps/api_manager/courses/tests.py
View file @
481a4b7e
...
...
@@ -3,12 +3,14 @@
Run these tests @ Devstack:
rake fasttest_lms[common/djangoapps/api_manager/courses/tests.py]
"""
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
import
json
import
uuid
import
mock
from
random
import
randint
from
urllib
import
urlencode
from
freezegun
import
freeze_time
from
dateutil.relativedelta
import
relativedelta
from
django.contrib.auth.models
import
Group
from
django.core.cache
import
cache
...
...
@@ -1972,6 +1974,144 @@ class CoursesApiTests(TestCase):
response
=
self
.
do_get
(
course_metrics_uri
)
self
.
assertEqual
(
response
.
status_code
,
404
)
@mock.patch.dict
(
"django.conf.settings.FEATURES"
,
{
'MARK_PROGRESS_ON_GRADING_EVENT'
:
True
,
'SIGNAL_ON_SCORE_CHANGED'
:
True
,
'STUDENT_GRADEBOOK'
:
True
,
'STUDENT_PROGRESS'
:
True
})
def
test_courses_data_time_series_metrics
(
self
):
course
=
CourseFactory
.
create
(
number
=
'3033'
,
name
=
'metrics_in_timeseries'
,
start
=
datetime
(
2014
,
9
,
16
,
14
,
30
),
end
=
datetime
(
2015
,
1
,
16
)
)
chapter
=
ItemFactory
.
create
(
category
=
"chapter"
,
parent_location
=
course
.
location
,
data
=
self
.
test_data
,
due
=
datetime
(
2015
,
5
,
16
,
14
,
30
),
display_name
=
"Overview"
)
sub_section
=
ItemFactory
.
create
(
parent_location
=
chapter
.
location
,
category
=
"sequential"
,
display_name
=
u"test subsection"
,
)
unit
=
ItemFactory
.
create
(
parent_location
=
sub_section
.
location
,
category
=
"vertical"
,
metadata
=
{
'graded'
:
True
,
'format'
:
'Homework'
},
display_name
=
u"test unit"
,
)
item
=
ItemFactory
.
create
(
parent_location
=
unit
.
location
,
category
=
'problem'
,
data
=
StringResponseXMLFactory
()
.
build_xml
(
answer
=
'bar'
),
display_name
=
'Problem to test timeseries'
,
metadata
=
{
'rerandomize'
:
'always'
,
'graded'
:
True
,
'format'
:
'Midterm Exam'
}
)
# create 10 users
USER_COUNT
=
25
users
=
[
UserFactory
.
create
(
username
=
"testuser_tstest"
+
str
(
__
),
profile
=
'test'
)
for
__
in
xrange
(
USER_COUNT
)]
# enroll users with time set to 28 days ago
enrolled_time
=
timezone
.
now
()
+
relativedelta
(
days
=-
28
)
with
freeze_time
(
enrolled_time
):
for
user
in
users
:
CourseEnrollmentFactory
.
create
(
user
=
user
,
course_id
=
course
.
id
)
# Mark users as those who have started course
for
j
,
user
in
enumerate
(
users
):
complete_time
=
timezone
.
now
()
+
relativedelta
(
days
=-
(
USER_COUNT
-
j
))
with
freeze_time
(
complete_time
):
points_scored
=
.
25
points_possible
=
1
module
=
self
.
get_module_for_user
(
user
,
course
,
item
)
grade_dict
=
{
'value'
:
points_scored
,
'max_value'
:
points_possible
,
'user_id'
:
user
.
id
}
module
.
system
.
publish
(
module
,
'grade'
,
grade_dict
)
# Last 2 users as those who have completed
if
j
>=
USER_COUNT
-
2
:
try
:
sg_entry
=
StudentGradebook
.
objects
.
get
(
user
=
user
,
course_id
=
course
.
id
)
sg_entry
.
grade
=
0.9
sg_entry
.
proforma_grade
=
0.91
sg_entry
.
save
()
except
StudentGradebook
.
DoesNotExist
:
StudentGradebook
.
objects
.
create
(
user
=
user
,
course_id
=
course
.
id
,
grade
=
0.9
,
proforma_grade
=
0.91
)
test_course_id
=
unicode
(
course
.
id
)
# get course metrics in time series format
end_date
=
datetime
.
now
()
.
date
()
start_date
=
end_date
+
relativedelta
(
days
=-
4
)
course_metrics_uri
=
'{}/{}/time-series-metrics/?start_date={}&end_date={}'
.
format
(
self
.
base_courses_uri
,
test_course_id
,
start_date
,
end_date
)
response
=
self
.
do_get
(
course_metrics_uri
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_not_started'
]),
5
)
total_not_started
=
sum
([
not_started
[
1
]
for
not_started
in
response
.
data
[
'users_not_started'
]])
self
.
assertEqual
(
total_not_started
,
6
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_started'
]),
5
)
total_started
=
sum
([
started
[
1
]
for
started
in
response
.
data
[
'users_started'
]])
self
.
assertEqual
(
total_started
,
4
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_completed'
]),
5
)
total_completed
=
sum
([
completed
[
1
]
for
completed
in
response
.
data
[
'users_completed'
]])
self
.
assertEqual
(
total_completed
,
2
)
# metrics with weeks as interval
start_date
=
end_date
+
relativedelta
(
days
=-
10
)
course_metrics_uri
=
'{}/{}/time-series-metrics/?start_date={}&end_date={}&'
\
'interval=weeks'
.
format
(
self
.
base_courses_uri
,
test_course_id
,
start_date
,
end_date
)
response
=
self
.
do_get
(
course_metrics_uri
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_not_started'
]),
2
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_started'
]),
2
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_completed'
]),
2
)
# metrics with months as interval
start_date
=
end_date
+
relativedelta
(
months
=-
3
)
end_date
=
datetime
.
now
()
.
date
()
+
relativedelta
(
months
=
1
)
course_metrics_uri
=
'{}/{}/time-series-metrics/?start_date={}&end_date={}&'
\
'interval=months'
.
format
(
self
.
base_courses_uri
,
test_course_id
,
start_date
,
end_date
)
response
=
self
.
do_get
(
course_metrics_uri
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_not_started'
]),
5
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_started'
]),
5
)
self
.
assertEqual
(
len
(
response
.
data
[
'users_completed'
]),
5
)
# test without end_date
course_metrics_uri
=
'{}/{}/time-series-metrics/?start_date={}'
.
format
(
self
.
base_courses_uri
,
test_course_id
,
start_date
)
response
=
self
.
do_get
(
course_metrics_uri
)
self
.
assertEqual
(
response
.
status_code
,
400
)
# test with unsupported interval
course_metrics_uri
=
'{}/{}/time-series-metrics/?start_date={}&end_date={}&interval=hours'
\
.
format
(
self
.
base_courses_uri
,
test_course_id
,
start_date
,
end_date
)
response
=
self
.
do_get
(
course_metrics_uri
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_course_workgroups_list
(
self
):
projects_uri
=
self
.
base_projects_uri
data
=
{
...
...
lms/djangoapps/api_manager/courses/urls.py
View file @
481a4b7e
...
...
@@ -24,6 +24,7 @@ urlpatterns = patterns(
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/completions/*$'
,
courses_views
.
CourseModuleCompletionList
.
as_view
(),
name
=
'completion-list'
),
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/projects/*$'
,
courses_views
.
CoursesProjectList
.
as_view
(),
name
=
'courseproject-list'
),
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/metrics/*$'
,
courses_views
.
CoursesMetrics
.
as_view
(),
name
=
'course-metrics'
),
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/time-series-metrics/*$'
,
courses_views
.
CoursesTimeSeriesMetrics
.
as_view
(),
name
=
'course-time-series-metrics'
),
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/metrics/cities/$'
,
courses_views
.
CoursesMetricsCities
.
as_view
(),
name
=
'courses-cities-metrics'
),
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/metrics/completions/leaders/*$'
,
courses_views
.
CoursesMetricsCompletionsLeadersList
.
as_view
(),
name
=
'course-metrics-completions-leaders'
),
url
(
r'^(?P<course_id>[a-zA-Z0-9_+\/:-]+)/metrics/grades/*$'
,
courses_views
.
CoursesMetricsGradesList
.
as_view
()),
...
...
lms/djangoapps/api_manager/courses/views.py
View file @
481a4b7e
...
...
@@ -13,7 +13,7 @@ from django.db.models import Avg, Count, Max, Min
from
django.http
import
Http404
from
django.utils
import
timezone
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.db.models
import
Q
from
django.db.models
import
Q
,
F
from
rest_framework
import
status
from
rest_framework.response
import
Response
...
...
@@ -36,7 +36,7 @@ from api_manager.models import CourseGroupRelationship, CourseContentGroupRelati
CourseModuleCompletion
from
api_manager.permissions
import
SecureAPIView
,
SecureListAPIView
from
api_manager.users.serializers
import
UserSerializer
,
UserCountByCitySerializer
from
api_manager.utils
import
generate_base_uri
,
str2bool
from
api_manager.utils
import
generate_base_uri
,
str2bool
,
get_time_series_data
,
parse_datetime
from
projects.models
import
Project
,
Workgroup
from
projects.serializers
import
ProjectSerializer
,
BasicWorkgroupSerializer
from
.serializers
import
CourseModuleCompletionSerializer
,
CourseSerializer
...
...
@@ -1539,6 +1539,80 @@ class CoursesMetrics(SecureAPIView):
data
.
update
(
thread_stats
)
return
Response
(
data
,
status
=
status
.
HTTP_200_OK
)
class
CoursesTimeSeriesMetrics
(
SecureAPIView
):
"""
### The CoursesTimeSeriesMetrics view allows clients to retrieve a list of Metrics for the specified Course
in time series format.
- URI: ```/api/courses/{course_id}/time-series-metrics/?start_date={date}&end_date={date}&interval={interval}&organization={organization_id}```
- interval can be `days`, `weeks` or `months`
- GET: Returns a JSON representation with three metrics
{
"users_not_started": [[datetime-1, count-1], [datetime-2, count-2], ........ [datetime-n, count-n]],
"users_started": [[datetime-1, count-1], [datetime-2, count-2], ........ [datetime-n, count-n]],
"users_completed": [[datetime-1, count-1], [datetime-2, count-2], ........ [datetime-n, count-n]]
}
- metrics can be filtered by organization by adding organization parameter to GET request
### Use Cases/Notes:
* Example: Display number of users completed, started or not started in a given course for a given time period
"""
def
get
(
self
,
request
,
course_id
):
# pylint: disable=W0613
"""
GET /api/courses/{course_id}/time-series-metrics/
"""
if
not
course_exists
(
request
,
request
.
user
,
course_id
):
return
Response
({},
status
=
status
.
HTTP_404_NOT_FOUND
)
start
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
,
None
)
end
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
,
None
)
interval
=
request
.
QUERY_PARAMS
.
get
(
'interval'
,
'days'
)
if
not
start
or
not
end
:
return
Response
({
"message"
:
_
(
"Both start_date and end_date parameters are required"
)}
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
if
interval
not
in
[
'days'
,
'weeks'
,
'months'
]:
return
Response
({
"message"
:
_
(
"Interval parameter is not valid. It should be one of these "
"'days', 'weeks', 'months'"
)},
status
=
status
.
HTTP_400_BAD_REQUEST
)
start_dt
=
parse_datetime
(
start
)
end_dt
=
parse_datetime
(
end
)
course_key
=
get_course_key
(
course_id
)
exclude_users
=
_get_aggregate_exclusion_user_ids
(
course_key
)
grade_complete_match_range
=
getattr
(
settings
,
'GRADEBOOK_GRADE_COMPLETE_PROFORMA_MATCH_RANGE'
,
0.01
)
grades_qs
=
StudentGradebook
.
objects
.
filter
(
course_id__exact
=
course_key
,
user__is_active
=
True
)
.
\
exclude
(
user_id__in
=
exclude_users
)
grades_complete_qs
=
grades_qs
.
filter
(
proforma_grade__lte
=
F
(
'grade'
)
+
grade_complete_match_range
,
proforma_grade__gt
=
0
)
enrolled_qs
=
CourseEnrollment
.
objects
.
filter
(
course_id__exact
=
course_key
,
user__is_active
=
True
)
\
.
exclude
(
id__in
=
exclude_users
)
users_started_qs
=
StudentProgress
.
objects
.
filter
(
course_id__exact
=
course_key
,
user__is_active
=
True
)
\
.
exclude
(
user_id__in
=
exclude_users
)
organization
=
request
.
QUERY_PARAMS
.
get
(
'organization'
,
None
)
if
organization
:
enrolled_qs
=
enrolled_qs
.
filter
(
user__organizations
=
organization
)
grades_complete_qs
=
grades_complete_qs
.
filter
(
user__organizations
=
organization
)
users_started_qs
=
users_started_qs
.
filter
(
user__organizations
=
organization
)
total_enrolled
=
enrolled_qs
.
filter
(
created__lt
=
start_dt
)
.
count
()
total_started
=
users_started_qs
.
filter
(
created__lt
=
start_dt
)
.
count
()
enrolled_series
=
get_time_series_data
(
enrolled_qs
,
start_dt
,
end_dt
,
interval
=
interval
,
date_field
=
'created'
,
aggregate
=
Count
(
'id'
))
started_series
=
get_time_series_data
(
users_started_qs
,
start_dt
,
end_dt
,
interval
=
interval
,
date_field
=
'created'
,
aggregate
=
Count
(
'id'
))
completed_series
=
get_time_series_data
(
grades_complete_qs
,
start_dt
,
end_dt
,
interval
=
interval
,
date_field
=
'modified'
,
aggregate
=
Count
(
'id'
))
not_started_series
=
[]
for
enrolled
,
started
in
zip
(
enrolled_series
,
started_series
):
not_started_series
.
append
((
started
[
0
],
(
total_enrolled
+
enrolled
[
1
])
-
(
total_started
+
started
[
1
])))
total_started
+=
started
[
1
]
total_enrolled
+=
enrolled
[
1
]
data
=
{
'users_not_started'
:
not_started_series
,
'users_started'
:
started_series
,
'users_completed'
:
completed_series
}
return
Response
(
data
,
status
=
status
.
HTTP_200_OK
)
class
CoursesMetricsGradesLeadersList
(
SecureListAPIView
):
"""
...
...
lms/djangoapps/api_manager/utils.py
View file @
481a4b7e
...
...
@@ -3,6 +3,11 @@
import
socket
import
struct
import
json
import
datetime
from
django.utils.timezone
import
now
from
dateutil.parser
import
parse
from
dateutil.relativedelta
import
relativedelta
,
MO
from
django.conf
import
settings
def
address_exists_in_network
(
ip_address
,
net_n_bits
):
...
...
@@ -87,3 +92,90 @@ def extract_data_params(request):
if
key
.
startswith
(
'data__'
):
data_params
.
append
({
key
[
6
:]:
val
})
return
data_params
def
strip_time
(
dt
):
"""
Removes time part of datetime
"""
tzinfo
=
getattr
(
dt
,
'tzinfo'
,
now
()
.
tzinfo
)
or
now
()
.
tzinfo
return
datetime
.
datetime
(
dt
.
year
,
dt
.
month
,
dt
.
day
,
tzinfo
=
tzinfo
)
def
parse_datetime
(
date_val
,
defaultdt
=
None
):
"""
Parses datetime value from string
"""
if
isinstance
(
date_val
,
basestring
):
return
parse
(
date_val
,
yearfirst
=
True
,
default
=
defaultdt
)
return
date_val
def
get_interval_bounds
(
date_val
,
interval
):
"""
Returns interval bounds the datetime is in.
"""
day
=
strip_time
(
date_val
)
if
interval
==
'day'
:
begin
=
day
end
=
day
+
relativedelta
(
days
=
1
)
elif
interval
==
'week'
:
begin
=
day
-
relativedelta
(
weekday
=
MO
(
-
1
))
end
=
begin
+
datetime
.
timedelta
(
days
=
7
)
elif
interval
==
'month'
:
begin
=
strip_time
(
datetime
.
datetime
(
date_val
.
year
,
date_val
.
month
,
1
,
tzinfo
=
date_val
.
tzinfo
))
end
=
begin
+
relativedelta
(
months
=
1
)
end
=
end
-
relativedelta
(
microseconds
=
1
)
return
begin
,
end
def
detect_db_engine
():
"""
detects database engine used
"""
engine
=
'mysql'
backend
=
settings
.
DATABASES
[
'default'
][
'ENGINE'
]
if
'sqlite'
in
backend
:
engine
=
'sqlite'
return
engine
def
get_time_series_data
(
queryset
,
start
,
end
,
interval
=
'days'
,
date_field
=
'created'
,
aggregate
=
None
):
"""
Aggregate over time intervals to compute time series representation of data
"""
engine
=
detect_db_engine
()
start
,
_
=
get_interval_bounds
(
start
,
interval
.
rstrip
(
's'
))
_
,
end
=
get_interval_bounds
(
end
,
interval
.
rstrip
(
's'
))
sql
=
{
'mysql'
:
{
'days'
:
"DATE_FORMAT(`{}`, '
%%
Y-
%%
m-
%%
d')"
.
format
(
date_field
),
'weeks'
:
"DATE_FORMAT(DATE_SUB(`{}`, INTERVAL(WEEKDAY(`{}`)) DAY), '
%%
Y-
%%
m-
%%
d')"
.
\
format
(
date_field
,
date_field
),
'months'
:
"DATE_FORMAT(`{}`, '
%%
Y-
%%
m-01')"
.
format
(
date_field
)
},
'sqlite'
:
{
'days'
:
"strftime('
%%
Y-
%%
m-
%%
d', `{}`)"
.
format
(
date_field
),
'weeks'
:
"strftime('
%%
Y-
%%
m-
%%
d', julianday(`{}`) - strftime('
%%
w', `{}`) + 1)"
.
format
(
date_field
,
date_field
),
'months'
:
"strftime('
%%
Y-
%%
m-01', `{}`)"
.
format
(
date_field
)
}
}
interval_sql
=
sql
[
engine
][
interval
]
kwargs
=
{
'{}__range'
.
format
(
date_field
):
(
start
,
end
)}
aggregate_data
=
queryset
.
extra
(
select
=
{
'd'
:
interval_sql
})
.
filter
(
**
kwargs
)
.
order_by
()
.
values
(
'd'
)
.
\
annotate
(
agg
=
aggregate
)
today
=
strip_time
(
now
())
data
=
dict
((
strip_time
(
parse_datetime
(
item
[
'd'
],
today
)),
item
[
'agg'
])
for
item
in
aggregate_data
)
series
=
[]
dt_key
=
start
while
dt_key
<
end
:
value
=
data
.
get
(
dt_key
,
0
)
series
.
append
((
dt_key
,
value
,))
dt_key
+=
relativedelta
(
**
{
interval
:
1
})
return
series
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