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
2760938e
Commit
2760938e
authored
Jul 29, 2013
by
Miles Steele
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix analytics distribution with no data, add unit test, cleanup
parent
f7989cb2
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
61 additions
and
31 deletions
+61
-31
lms/djangoapps/analytics/basic.py
+1
-1
lms/djangoapps/analytics/distributions.py
+37
-19
lms/djangoapps/analytics/tests/test_distributions.py
+16
-2
lms/djangoapps/instructor/access.py
+4
-4
lms/djangoapps/instructor/views/instructor_dashboard.py
+3
-2
lms/envs/common.py
+0
-3
No files found.
lms/djangoapps/analytics/basic.py
View file @
2760938e
...
@@ -36,7 +36,7 @@ def enrolled_students_features(course_id, features):
...
@@ -36,7 +36,7 @@ def enrolled_students_features(course_id, features):
student_dict
=
dict
((
feature
,
getattr
(
student
,
feature
))
student_dict
=
dict
((
feature
,
getattr
(
student
,
feature
))
for
feature
in
student_features
)
for
feature
in
student_features
)
profile
=
student
.
profile
profile
=
student
.
profile
if
not
profile
is
None
:
if
profile
is
not
None
:
profile_dict
=
dict
((
feature
,
getattr
(
profile
,
feature
))
profile_dict
=
dict
((
feature
,
getattr
(
profile
,
feature
))
for
feature
in
profile_features
)
for
feature
in
profile_features
)
student_dict
.
update
(
profile_dict
)
student_dict
.
update
(
profile_dict
)
...
...
lms/djangoapps/analytics/distributions.py
View file @
2760938e
...
@@ -24,8 +24,11 @@ The distribution in a course for gender might look like:
...
@@ -24,8 +24,11 @@ The distribution in a course for gender might look like:
from
django.db.models
import
Count
from
django.db.models
import
Count
from
student.models
import
CourseEnrollment
,
UserProfile
from
student.models
import
CourseEnrollment
,
UserProfile
# choices with a restricted domain, e.g. level_of_education
_EASY_CHOICE_FEATURES
=
(
'gender'
,
'level_of_education'
)
_EASY_CHOICE_FEATURES
=
(
'gender'
,
'level_of_education'
)
# choices with a larger domain e.g. year_of_birth
_OPEN_CHOICE_FEATURES
=
(
'year_of_birth'
,)
_OPEN_CHOICE_FEATURES
=
(
'year_of_birth'
,)
AVAILABLE_PROFILE_FEATURES
=
_EASY_CHOICE_FEATURES
+
_OPEN_CHOICE_FEATURES
AVAILABLE_PROFILE_FEATURES
=
_EASY_CHOICE_FEATURES
+
_OPEN_CHOICE_FEATURES
DISPLAY_NAMES
=
{
DISPLAY_NAMES
=
{
'gender'
:
'Gender'
,
'gender'
:
'Gender'
,
...
@@ -51,7 +54,7 @@ class ProfileDistribution(object):
...
@@ -51,7 +54,7 @@ class ProfileDistribution(object):
def
__init__
(
self
,
feature
):
def
__init__
(
self
,
feature
):
self
.
feature
=
feature
self
.
feature
=
feature
self
.
feature_display_name
=
DISPLAY_NAMES
[
feature
]
self
.
feature_display_name
=
DISPLAY_NAMES
.
get
(
feature
,
feature
)
def
validate
(
self
):
def
validate
(
self
):
"""
"""
...
@@ -64,6 +67,7 @@ class ProfileDistribution(object):
...
@@ -64,6 +67,7 @@ class ProfileDistribution(object):
raise
ProfileDistribution
.
ValidationError
()
raise
ProfileDistribution
.
ValidationError
()
validation_assert
(
isinstance
(
self
.
feature
,
str
))
validation_assert
(
isinstance
(
self
.
feature
,
str
))
validation_assert
(
self
.
feature
in
DISPLAY_NAMES
)
validation_assert
(
isinstance
(
self
.
feature_display_name
,
str
))
validation_assert
(
isinstance
(
self
.
feature_display_name
,
str
))
validation_assert
(
self
.
type
in
[
'EASY_CHOICE'
,
'OPEN_CHOICE'
])
validation_assert
(
self
.
type
in
[
'EASY_CHOICE'
,
'OPEN_CHOICE'
])
validation_assert
(
isinstance
(
self
.
data
,
dict
))
validation_assert
(
isinstance
(
self
.
data
,
dict
))
...
@@ -78,10 +82,8 @@ def profile_distribution(course_id, feature):
...
@@ -78,10 +82,8 @@ def profile_distribution(course_id, feature):
Returns a ProfileDistribution instance.
Returns a ProfileDistribution instance.
NOTE: no_data will appear as a key instead of None to be compatible with the json spec.
NOTE: no_data will appear as a key instead of None/null to adhere to the json spec.
data types are
data types are EASY_CHOICE or OPEN_CHOICE
EASY_CHOICE - choices with a restricted domain, e.g. level_of_education
OPEN_CHOICE - choices with a larger domain e.g. year_of_birth
"""
"""
if
not
feature
in
AVAILABLE_PROFILE_FEATURES
:
if
not
feature
in
AVAILABLE_PROFILE_FEATURES
:
...
@@ -100,28 +102,41 @@ def profile_distribution(course_id, feature):
...
@@ -100,28 +102,41 @@ def profile_distribution(course_id, feature):
elif
feature
==
'level_of_education'
:
elif
feature
==
'level_of_education'
:
raw_choices
=
UserProfile
.
LEVEL_OF_EDUCATION_CHOICES
raw_choices
=
UserProfile
.
LEVEL_OF_EDUCATION_CHOICES
# short name and display nae (full) of the choices.
# short name and display na
m
e (full) of the choices.
choices
=
[(
short
,
full
)
choices
=
[(
short
,
full
)
for
(
short
,
full
)
in
raw_choices
]
+
[(
'no_data'
,
'No Data'
)]
for
(
short
,
full
)
in
raw_choices
]
+
[(
'no_data'
,
'No Data'
)]
def
get_filter
(
feature
,
value
):
""" Get the orm filter parameters for a feature. """
return
{
'gender'
:
{
'user__profile__gender'
:
value
},
'level_of_education'
:
{
'user__profile__level_of_education'
:
value
},
}[
feature
]
def
get_count
(
feature
,
value
):
""" Get the count of enrolled students matching the feature value. """
return
CourseEnrollment
.
objects
.
filter
(
course_id
=
course_id
,
**
get_filter
(
feature
,
value
)
)
.
count
()
distribution
=
{}
distribution
=
{}
for
(
short
,
full
)
in
choices
:
for
(
short
,
full
)
in
choices
:
if
feature
==
'gender'
:
# handle no data case
count
=
CourseEnrollment
.
objects
.
filter
(
if
short
==
'no_data'
:
course_id
=
course_id
,
user__profile__gender
=
short
distribution
[
'no_data'
]
=
0
)
.
count
()
distribution
[
'no_data'
]
+=
get_count
(
feature
,
None
)
elif
feature
==
'level_of_education'
:
distribution
[
'no_data'
]
+=
get_count
(
feature
,
''
)
count
=
CourseEnrollment
.
objects
.
filter
(
else
:
course_id
=
course_id
,
user__profile__level_of_education
=
short
distribution
[
short
]
=
get_count
(
feature
,
short
)
)
.
count
()
distribution
[
short
]
=
count
prd
.
data
=
distribution
prd
.
data
=
distribution
prd
.
choices_display_names
=
dict
(
choices
)
prd
.
choices_display_names
=
dict
(
choices
)
elif
feature
in
_OPEN_CHOICE_FEATURES
:
elif
feature
in
_OPEN_CHOICE_FEATURES
:
prd
.
type
=
'OPEN_CHOICE'
prd
.
type
=
'OPEN_CHOICE'
profiles
=
UserProfile
.
objects
.
filter
(
profiles
=
UserProfile
.
objects
.
filter
(
user__courseenrollment__course_id
=
course_id
)
user__courseenrollment__course_id
=
course_id
)
query_distribution
=
profiles
.
values
(
query_distribution
=
profiles
.
values
(
feature
)
.
annotate
(
Count
(
feature
))
.
order_by
()
feature
)
.
annotate
(
Count
(
feature
))
.
order_by
()
# query_distribution is of the form [{'featureval': 'value1', 'featureval__count': 4},
# query_distribution is of the form [{'featureval': 'value1', 'featureval__count': 4},
...
@@ -133,9 +148,12 @@ def profile_distribution(course_id, feature):
...
@@ -133,9 +148,12 @@ def profile_distribution(course_id, feature):
# change none to no_data for valid json key
# change none to no_data for valid json key
if
None
in
distribution
:
if
None
in
distribution
:
distribution
[
'no_data'
]
=
distribution
.
pop
(
None
)
# django does not properly count NULL values when using annotate Count
# django does not properly count NULL values, so the above will alwasy be 0.
# so
# this correctly counts null values
# distribution['no_data'] = distribution.pop(None)
# would always be 0.
# Correctly count null values
distribution
[
'no_data'
]
=
profiles
.
filter
(
distribution
[
'no_data'
]
=
profiles
.
filter
(
**
{
feature
:
None
}
**
{
feature
:
None
}
)
.
count
()
)
.
count
()
...
...
lms/djangoapps/analytics/tests/test_distributions.py
View file @
2760938e
...
@@ -19,7 +19,10 @@ class TestAnalyticsDistributions(TestCase):
...
@@ -19,7 +19,10 @@ class TestAnalyticsDistributions(TestCase):
profile__year_of_birth
=
i
+
1930
profile__year_of_birth
=
i
+
1930
)
for
i
in
xrange
(
30
)]
)
for
i
in
xrange
(
30
)]
self
.
ces
=
[
CourseEnrollment
.
objects
.
create
(
course_id
=
self
.
course_id
,
user
=
user
)
for
user
in
self
.
users
]
self
.
ces
=
[
CourseEnrollment
.
objects
.
create
(
course_id
=
self
.
course_id
,
user
=
user
)
for
user
in
self
.
users
]
@raises
(
ValueError
)
@raises
(
ValueError
)
def
test_profile_distribution_bad_feature
(
self
):
def
test_profile_distribution_bad_feature
(
self
):
...
@@ -59,12 +62,23 @@ class TestAnalyticsDistributionsNoData(TestCase):
...
@@ -59,12 +62,23 @@ class TestAnalyticsDistributionsNoData(TestCase):
self
.
nodata_users
=
[
UserFactory
(
self
.
nodata_users
=
[
UserFactory
(
profile__year_of_birth
=
None
,
profile__year_of_birth
=
None
,
)
for
_
in
xrange
(
4
)]
profile__gender
=
[
None
,
''
][
i
%
2
]
)
for
i
in
xrange
(
4
)]
self
.
users
+=
self
.
nodata_users
self
.
users
+=
self
.
nodata_users
self
.
ces
=
tuple
(
CourseEnrollment
.
objects
.
create
(
course_id
=
self
.
course_id
,
user
=
user
)
for
user
in
self
.
users
)
self
.
ces
=
tuple
(
CourseEnrollment
.
objects
.
create
(
course_id
=
self
.
course_id
,
user
=
user
)
for
user
in
self
.
users
)
def
test_profile_distribution_easy_choice_nodata
(
self
):
feature
=
'gender'
self
.
assertIn
(
feature
,
AVAILABLE_PROFILE_FEATURES
)
distribution
=
profile_distribution
(
self
.
course_id
,
feature
)
print
distribution
self
.
assertEqual
(
distribution
.
type
,
'EASY_CHOICE'
)
self
.
assertTrue
(
hasattr
(
distribution
,
'choices_display_names'
))
self
.
assertIn
(
'no_data'
,
distribution
.
data
)
self
.
assertEqual
(
distribution
.
data
[
'no_data'
],
len
(
self
.
nodata_users
))
def
test_profile_distribution_open_choice_nodata
(
self
):
def
test_profile_distribution_open_choice_nodata
(
self
):
feature
=
'year_of_birth'
feature
=
'year_of_birth'
self
.
assertIn
(
feature
,
AVAILABLE_PROFILE_FEATURES
)
self
.
assertIn
(
feature
,
AVAILABLE_PROFILE_FEATURES
)
...
...
lms/djangoapps/instructor/access.py
View file @
2760938e
...
@@ -22,7 +22,7 @@ def list_with_level(course, level):
...
@@ -22,7 +22,7 @@ def list_with_level(course, level):
"""
"""
List users who have 'level' access.
List users who have 'level' access.
level
is in ['instructor', 'staff', 'beta'] for standard courses.
`level`
is in ['instructor', 'staff', 'beta'] for standard courses.
There could be other levels specific to the course.
There could be other levels specific to the course.
If there is no Group for that course-level, returns an empty list
If there is no Group for that course-level, returns an empty list
"""
"""
...
@@ -42,7 +42,7 @@ def allow_access(course, user, level):
...
@@ -42,7 +42,7 @@ def allow_access(course, user, level):
"""
"""
Allow user access to course modification.
Allow user access to course modification.
level
is one of ['instructor', 'staff', 'beta']
`level`
is one of ['instructor', 'staff', 'beta']
"""
"""
_change_access
(
course
,
user
,
level
,
'allow'
)
_change_access
(
course
,
user
,
level
,
'allow'
)
...
@@ -51,7 +51,7 @@ def revoke_access(course, user, level):
...
@@ -51,7 +51,7 @@ def revoke_access(course, user, level):
"""
"""
Revoke access from user to course modification.
Revoke access from user to course modification.
level
is one of ['instructor', 'staff', 'beta']
`level`
is one of ['instructor', 'staff', 'beta']
"""
"""
_change_access
(
course
,
user
,
level
,
'revoke'
)
_change_access
(
course
,
user
,
level
,
'revoke'
)
...
@@ -60,7 +60,7 @@ def _change_access(course, user, level, action):
...
@@ -60,7 +60,7 @@ def _change_access(course, user, level, action):
"""
"""
Change access of user.
Change access of user.
level
is one of ['instructor', 'staff', 'beta']
`level`
is one of ['instructor', 'staff', 'beta']
action is one of ['allow', 'revoke']
action is one of ['allow', 'revoke']
NOTE: will create a group if it does not yet exist.
NOTE: will create a group if it does not yet exist.
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
2760938e
...
@@ -49,7 +49,6 @@ def instructor_dashboard_2(request, course_id):
...
@@ -49,7 +49,6 @@ def instructor_dashboard_2(request, course_id):
context
=
{
context
=
{
'course'
:
course
,
'course'
:
course
,
# 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'old_dashboard_url'
:
reverse
(
'instructor_dashboard'
,
kwargs
=
{
'course_id'
:
course_id
}),
'old_dashboard_url'
:
reverse
(
'instructor_dashboard'
,
kwargs
=
{
'course_id'
:
course_id
}),
'sections'
:
sections
,
'sections'
:
sections
,
}
}
...
@@ -83,7 +82,9 @@ def _section_course_info(course_id):
...
@@ -83,7 +82,9 @@ def _section_course_info(course_id):
section_data
[
'has_started'
]
=
course
.
has_started
()
section_data
[
'has_started'
]
=
course
.
has_started
()
section_data
[
'has_ended'
]
=
course
.
has_ended
()
section_data
[
'has_ended'
]
=
course
.
has_ended
()
try
:
try
:
section_data
[
'grade_cutoffs'
]
=
""
+
reduce
(
lambda
memo
,
(
letter
,
score
):
"{}: {}, "
.
format
(
letter
,
score
)
+
memo
,
course
.
grade_cutoffs
.
items
(),
""
)[:
-
2
]
+
""
def
next
(
memo
,
(
letter
,
score
)):
return
"{}: {}, "
.
format
(
letter
,
score
)
+
memo
section_data
[
'grade_cutoffs'
]
=
reduce
(
next
,
course
.
grade_cutoffs
.
items
(),
""
)[:
-
2
]
except
:
except
:
section_data
[
'grade_cutoffs'
]
=
"Not Available"
section_data
[
'grade_cutoffs'
]
=
"Not Available"
# section_data['offline_grades'] = offline_grades_available(course_id)
# section_data['offline_grades'] = offline_grades_available(course_id)
...
...
lms/envs/common.py
View file @
2760938e
...
@@ -156,9 +156,6 @@ MITX_FEATURES = {
...
@@ -156,9 +156,6 @@ MITX_FEATURES = {
# Toggle to enable chat availability (configured on a per-course
# Toggle to enable chat availability (configured on a per-course
# basis in Studio)
# basis in Studio)
'ENABLE_CHAT'
:
False
,
'ENABLE_CHAT'
:
False
,
# Enable instructor dash to submit background tasks
'ENABLE_INSTRUCTOR_BETA_DASHBOARD'
:
False
,
}
}
# Used for A/B testing
# Used for A/B testing
...
...
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