Commit 7392a888 by Miles Steele

cleanup, fix permissions, rename functions, add more api tests

parent 1e8cb7df
...@@ -36,9 +36,10 @@ def enrolled_students_features(course_id, features): ...@@ -36,9 +36,10 @@ 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
profile_dict = dict((feature, getattr(profile, feature)) if not profile is None:
for feature in profile_features) profile_dict = dict((feature, getattr(profile, feature))
student_dict.update(profile_dict) for feature in profile_features)
student_dict.update(profile_dict)
return student_dict return student_dict
return [extract_student(student, features) for student in students] return [extract_student(student, features) for student in students]
......
""" """
Profile Distributions Profile Distributions
Aggregate sums for values of fields in students profiles.
For example:
The distribution in a course for gender might look like:
'gender': {
'type': 'EASY_CHOICE',
'data': {
'no_data': 1234,
'm': 5678,
'o': 2134,
'f': 5678
},
'display_names': {
'no_data': 'No Data',
'm': 'Male',
'o': 'Other',
'f': 'Female'
}
""" """
from django.db.models import Count from django.db.models import Count
...@@ -8,6 +27,48 @@ from student.models import CourseEnrollment, UserProfile ...@@ -8,6 +27,48 @@ from student.models import CourseEnrollment, UserProfile
_EASY_CHOICE_FEATURES = ('gender', 'level_of_education') _EASY_CHOICE_FEATURES = ('gender', 'level_of_education')
_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 = {
'gender': 'Gender',
'level_of_education': 'Level of Education',
'year_of_birth': 'Year Of Birth',
}
class ProfileDistribution(object):
"""
Container for profile distribution data
`feature` is the name of the distribution feature
`feature_display_name` is the display name of feature
`data` is a dictionary of the distribution
`type` is either 'EASY_CHOICE' or 'OPEN_CHOICE'
`choices_display_names` is a dict if the distribution is an 'EASY_CHOICE'
"""
class ValidationError(ValueError):
""" Error thrown if validation fails. """
pass
def __init__(self, feature):
self.feature = feature
self.feature_display_name = DISPLAY_NAMES[feature]
def validate(self):
"""
Validate this profile distribution.
Throws ProfileDistribution.ValidationError
"""
def validation_assert(predicate):
if not predicate:
raise ProfileDistribution.ValidationError()
validation_assert(isinstance(self.feature, str))
validation_assert(isinstance(self.feature_display_name, str))
validation_assert(self.type in ['EASY_CHOICE', 'OPEN_CHOICE'])
validation_assert(isinstance(self.data, dict))
if self.type == 'EASY_CHOICE':
validation_assert(isinstance(self.choices_display_names, dict))
def profile_distribution(course_id, feature): def profile_distribution(course_id, feature):
...@@ -15,37 +76,35 @@ def profile_distribution(course_id, feature): ...@@ -15,37 +76,35 @@ def profile_distribution(course_id, feature):
Retrieve distribution of students over a given feature. Retrieve distribution of students over a given feature.
feature is one of AVAILABLE_PROFILE_FEATURES. feature is one of AVAILABLE_PROFILE_FEATURES.
Return a dictionary { Returns a ProfileDistribution instance.
'type': 'SOME_TYPE',
'data': {'key': 'val'},
'display_names': {'key': 'displaynameval'}
}
display_names is only return for EASY_CHOICE type eatuers NOTE: no_data will appear as a key instead of None to be compatible with the json spec.
note no_data instead of None to be compatible with the json spec. data types are
data types e.g.
EASY_CHOICE - choices with a restricted domain, e.g. level_of_education EASY_CHOICE - choices with a restricted domain, e.g. level_of_education
OPEN_CHOICE - choices with a larger domain e.g. year_of_birth OPEN_CHOICE - choices with a larger domain e.g. year_of_birth
""" """
feature_results = {}
if not feature in AVAILABLE_PROFILE_FEATURES: if not feature in AVAILABLE_PROFILE_FEATURES:
raise ValueError( raise ValueError(
"unsupported feature requested for distribution '{}'".format( "unsupported feature requested for distribution '{}'".format(
feature) feature)
) )
prd = ProfileDistribution(feature)
if feature in _EASY_CHOICE_FEATURES: if feature in _EASY_CHOICE_FEATURES:
prd.type = 'EASY_CHOICE'
if feature == 'gender': if feature == 'gender':
raw_choices = UserProfile.GENDER_CHOICES raw_choices = UserProfile.GENDER_CHOICES
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.
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')]
data = {} distribution = {}
for (short, full) in choices: for (short, full) in choices:
if feature == 'gender': if feature == 'gender':
count = CourseEnrollment.objects.filter( count = CourseEnrollment.objects.filter(
...@@ -55,12 +114,12 @@ def profile_distribution(course_id, feature): ...@@ -55,12 +114,12 @@ def profile_distribution(course_id, feature):
count = CourseEnrollment.objects.filter( count = CourseEnrollment.objects.filter(
course_id=course_id, user__profile__level_of_education=short course_id=course_id, user__profile__level_of_education=short
).count() ).count()
data[short] = count distribution[short] = count
feature_results['data'] = data prd.data = distribution
feature_results['type'] = 'EASY_CHOICE' prd.choices_display_names = dict(choices)
feature_results['display_names'] = dict(choices)
elif feature in _OPEN_CHOICE_FEATURES: elif feature in _OPEN_CHOICE_FEATURES:
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(
...@@ -81,7 +140,7 @@ def profile_distribution(course_id, feature): ...@@ -81,7 +140,7 @@ def profile_distribution(course_id, feature):
**{feature: None} **{feature: None}
).count() ).count()
feature_results['data'] = distribution prd.data = distribution
feature_results['type'] = 'OPEN_CHOICE'
return feature_results prd.validate()
return prd
...@@ -31,20 +31,20 @@ class TestAnalyticsDistributions(TestCase): ...@@ -31,20 +31,20 @@ class TestAnalyticsDistributions(TestCase):
feature = 'gender' feature = 'gender'
self.assertIn(feature, AVAILABLE_PROFILE_FEATURES) self.assertIn(feature, AVAILABLE_PROFILE_FEATURES)
distribution = profile_distribution(self.course_id, feature) distribution = profile_distribution(self.course_id, feature)
self.assertEqual(distribution['type'], 'EASY_CHOICE') self.assertEqual(distribution.type, 'EASY_CHOICE')
self.assertEqual(distribution['data']['no_data'], 0) self.assertEqual(distribution.data['no_data'], 0)
self.assertEqual(distribution['data']['m'], len(self.users) / 3) self.assertEqual(distribution.data['m'], len(self.users) / 3)
self.assertEqual(distribution['display_names']['m'], 'Male') self.assertEqual(distribution.choices_display_names['m'], 'Male')
def test_profile_distribution_open_choice(self): def test_profile_distribution_open_choice(self):
feature = 'year_of_birth' feature = 'year_of_birth'
self.assertIn(feature, AVAILABLE_PROFILE_FEATURES) self.assertIn(feature, AVAILABLE_PROFILE_FEATURES)
distribution = profile_distribution(self.course_id, feature) distribution = profile_distribution(self.course_id, feature)
print distribution print distribution
self.assertEqual(distribution['type'], 'OPEN_CHOICE') self.assertEqual(distribution.type, 'OPEN_CHOICE')
self.assertNotIn('display_names', distribution) self.assertFalse(hasattr(distribution, 'choices_display_names'))
self.assertNotIn('no_data', distribution['data']) self.assertNotIn('no_data', distribution.data)
self.assertEqual(distribution['data'][1930], 1) self.assertEqual(distribution.data[1930], 1)
class TestAnalyticsDistributionsNoData(TestCase): class TestAnalyticsDistributionsNoData(TestCase):
...@@ -70,7 +70,7 @@ class TestAnalyticsDistributionsNoData(TestCase): ...@@ -70,7 +70,7 @@ class TestAnalyticsDistributionsNoData(TestCase):
self.assertIn(feature, AVAILABLE_PROFILE_FEATURES) self.assertIn(feature, AVAILABLE_PROFILE_FEATURES)
distribution = profile_distribution(self.course_id, feature) distribution = profile_distribution(self.course_id, feature)
print distribution print distribution
self.assertEqual(distribution['type'], 'OPEN_CHOICE') self.assertEqual(distribution.type, 'OPEN_CHOICE')
self.assertNotIn('display_names', distribution) self.assertFalse(hasattr(distribution, 'choices_display_names'))
self.assertIn('no_data', distribution['data']) self.assertIn('no_data', distribution.data)
self.assertEqual(distribution['data']['no_data'], len(self.nodata_users)) self.assertEqual(distribution.data['no_data'], len(self.nodata_users))
...@@ -58,6 +58,8 @@ def _change_access(course, user, level, mode): ...@@ -58,6 +58,8 @@ def _change_access(course, user, level, mode):
level is one of ['instructor', 'staff', 'beta'] level is one of ['instructor', 'staff', 'beta']
mode is one of ['allow', 'revoke'] mode is one of ['allow', 'revoke']
NOTE: will NOT create a group that does not yet exist.
""" """
if level is 'beta': if level is 'beta':
......
""" """
Unit tests for instructor.enrollment methods. Unit tests for instructor.api methods.
""" """
import json
from urllib import quote
from django.test import TestCase from django.test import TestCase
from nose.tools import raises
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from instructor.views.api import _split_input_list from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.helpers import LoginEnrollmentTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, AdminFactory
from student.models import CourseEnrollment
from instructor.views.api import _split_input_list, _msk_from_problem_urlname
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Ensure that users cannot access endpoints they shouldn't be able to.
"""
def setUp(self):
self.user = UserFactory.create()
self.course = CourseFactory.create()
CourseEnrollment.objects.create(user=self.user, course_id=self.course.id)
self.client.login(username=self.user.username, password='test')
def test_deny_students_update_enrollment(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {})
self.assertEqual(response.status_code, 403)
def test_staff_level(self):
"""
Ensure that an enrolled student can't access staff or instructor endpoints.
"""
staff_level_endpoints = [
'students_update_enrollment',
'modify_access',
'list_course_role_members',
'get_grading_config',
'get_students_features',
'get_distribution',
'get_student_progress_url',
'reset_student_attempts',
'rescore_problem',
'list_instructor_tasks',
'list_forum_members',
'update_forum_role_membership',
]
for endpoint in staff_level_endpoints:
url = reverse(endpoint, kwargs={'course_id': self.course.id})
response = self.client.get(url, {})
self.assertEqual(response.status_code, 403)
def test_instructor_level(self):
"""
Ensure that a staff member can't access instructor endpoints.
"""
instructor_level_endpoints = [
'modify_access',
'list_course_role_members',
'reset_student_attempts',
'list_instructor_tasks',
'update_forum_role_membership',
]
for endpoint in instructor_level_endpoints:
url = reverse(endpoint, kwargs={'course_id': self.course.id})
response = self.client.get(url, {})
self.assertEqual(response.status_code, 403)
# class TestInstructorAPILevelsEnrollment
# # students_update_enrollment
# class TestInstructorAPILevelsAccess
# # modify_access
# # list_course_role_members
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test endpoints that show data without side effects.
"""
def setUp(self):
self.instructor = AdminFactory.create()
self.course = CourseFactory.create()
self.client.login(username=self.instructor.username, password='test')
# self.students = [UserFactory(email="foobar{}@robot.org".format(i)) for i in xrange(6)]
self.students = [UserFactory() for _ in xrange(6)]
for student in self.students:
CourseEnrollment.objects.create(user=student, course_id=self.course.id)
def test_get_students_features(self):
"""
Test that some minimum of information is formatted
correctly in the response to get_students_features.
"""
url = reverse('get_students_features', kwargs={'course_id': self.course.id})
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('students', res_json)
for student in self.students:
student_json = [
x for x in res_json['students']
if x['username'] == student.username
][0]
self.assertEqual(student_json['username'], student.username)
self.assertEqual(student_json['email'], student.email)
def test_get_distribution_no_feature(self):
"""
Test that get_distribution lists available features
when supplied no feature quparameter.
"""
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
res_json = json.loads(response.content)
self.assertEqual(type(res_json['available_features']), list)
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
response = self.client.get(url + u'?feature=')
self.assertEqual(response.status_code, 200)
res_json = json.loads(response.content)
self.assertEqual(type(res_json['available_features']), list)
def test_get_distribution_unavailable_feature(self):
"""
Test that get_distribution fails gracefully with
an unavailable feature.
"""
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'feature': 'robot-not-a-real-feature'})
self.assertEqual(response.status_code, 400)
def test_get_distribution_gender(self):
"""
Test that get_distribution fails gracefully with
an unavailable feature.
"""
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'feature': 'gender'})
self.assertEqual(response.status_code, 200)
res_json = json.loads(response.content)
print res_json
self.assertEqual(res_json['feature_results']['data']['m'], 6)
self.assertEqual(res_json['feature_results']['choices_display_names']['m'], 'Male')
self.assertEqual(res_json['feature_results']['data']['no_data'], 0)
self.assertEqual(res_json['feature_results']['choices_display_names']['no_data'], 'No Data')
def test_get_student_progress_url(self):
""" Test that progress_url is in the successful response. """
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
url += "?student_email={}".format(
quote(self.students[0].email.encode("utf-8"))
)
print url
response = self.client.get(url)
print response
self.assertEqual(response.status_code, 200)
res_json = json.loads(response.content)
self.assertIn('progress_url', res_json)
def test_get_student_progress_url_noparams(self):
""" Test that the endpoint 404's without the required query params. """
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 400)
def test_get_student_progress_url_nostudent(self):
""" Test that the endpoint 400's when requesting an unknown email. """
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 400)
# class TestInstructorAPILevelsGrade modification & tasks
# # reset_student_attempts
# # rescore_problem
# # list_instructor_tasks
# class TestInstructorAPILevelsForums
# # list_forum_members
# # update_forum_role_membership
class TestInstructorAPIHelpers(TestCase): class TestInstructorAPIHelpers(TestCase):
...@@ -24,3 +208,13 @@ class TestInstructorAPIHelpers(TestCase): ...@@ -24,3 +208,13 @@ class TestInstructorAPIHelpers(TestCase):
self.assertEqual(_split_input_list(u'robot@robot.edu, robot2@robot.edu'), [u'robot@robot.edu', 'robot2@robot.edu']) self.assertEqual(_split_input_list(u'robot@robot.edu, robot2@robot.edu'), [u'robot@robot.edu', 'robot2@robot.edu'])
scary_unistuff = unichr(40960) + u'abcd' + unichr(1972) scary_unistuff = unichr(40960) + u'abcd' + unichr(1972)
self.assertEqual(_split_input_list(scary_unistuff), [scary_unistuff]) self.assertEqual(_split_input_list(scary_unistuff), [scary_unistuff])
def test_msk_from_problem_urlname(self):
args = ('MITx/6.002x/2013_Spring', 'L2Node1')
output = 'i4x://MITx/6.002x/problem/L2Node1'
self.assertEqual(_msk_from_problem_urlname(*args), output)
@raises(ValueError)
def test_msk_from_problem_urlname_error(self):
args = ('notagoodcourse', 'L2Node1')
_msk_from_problem_urlname(*args)
...@@ -108,7 +108,7 @@ def _section_membership(course_id, access): ...@@ -108,7 +108,7 @@ def _section_membership(course_id, access):
'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), 'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), 'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}), 'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}),
'access_allow_revoke_url': reverse('access_allow_revoke', kwargs={'course_id': course_id}), 'modify_access_url': reverse('modify_access', kwargs={'course_id': course_id}),
'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_id}), 'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_id}),
'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_id}), 'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_id}),
} }
...@@ -121,7 +121,7 @@ def _section_student_admin(course_id, access): ...@@ -121,7 +121,7 @@ def _section_student_admin(course_id, access):
'section_key': 'student_admin', 'section_key': 'student_admin',
'section_display_name': 'Student Admin', 'section_display_name': 'Student Admin',
'access': access, 'access': access,
'get_student_progress_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}), 'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}),
'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), 'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}), 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}),
'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_id}), 'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_id}),
...@@ -135,8 +135,8 @@ def _section_data_download(course_id): ...@@ -135,8 +135,8 @@ def _section_data_download(course_id):
section_data = { section_data = {
'section_key': 'data_download', 'section_key': 'data_download',
'section_display_name': 'Data Download', 'section_display_name': 'Data Download',
'grading_config_url': reverse('grading_config', kwargs={'course_id': course_id}), 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}),
'enrolled_students_features_url': reverse('enrolled_students_features', kwargs={'course_id': course_id}), 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}),
} }
return section_data return section_data
...@@ -146,6 +146,6 @@ def _section_analytics(course_id): ...@@ -146,6 +146,6 @@ def _section_analytics(course_id):
section_data = { section_data = {
'section_key': 'analytics', 'section_key': 'analytics',
'section_display_name': 'Analytics', 'section_display_name': 'Analytics',
'profile_distributions_url': reverse('profile_distribution', kwargs={'course_id': course_id}), 'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}),
} }
return section_data return section_data
...@@ -29,6 +29,8 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True ...@@ -29,6 +29,8 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it. # Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED = True WIKI_ENABLED = True
......
...@@ -28,7 +28,8 @@ class Analytics ...@@ -28,7 +28,8 @@ class Analytics
# fetch and list available distributions # fetch and list available distributions
# `cb` is a callback to be run after # `cb` is a callback to be run after
populate_selector: (cb) -> populate_selector: (cb) ->
@get_profile_distributions [], # ask for no particular distribution to get list of available distribuitions.
@get_profile_distributions undefined,
# on error, print to console and dom. # on error, print to console and dom.
error: std_ajax_err => @$request_response_error.text "Error getting available distributions." error: std_ajax_err => @$request_response_error.text "Error getting available distributions."
success: (data) => success: (data) =>
...@@ -38,7 +39,7 @@ class Analytics ...@@ -38,7 +39,7 @@ class Analytics
# add all fetched available features to drop-down # add all fetched available features to drop-down
for feature in data.available_features for feature in data.available_features
opt = $ '<option/>', opt = $ '<option/>',
text: data.display_names[feature] text: data.feature_display_names[feature]
data: data:
feature: feature feature: feature
...@@ -53,64 +54,60 @@ class Analytics ...@@ -53,64 +54,60 @@ class Analytics
feature = opt.data 'feature' feature = opt.data 'feature'
@reset_display() @reset_display()
# only proceed if there is a feature attached to the selected option.
return unless feature return unless feature
@get_profile_distributions [feature], @get_profile_distributions feature,
error: std_ajax_err => @$request_response_error.text "Error getting distribution for '#{feature}'." error: std_ajax_err => @$request_response_error.text "Error getting distribution for '#{feature}'."
success: (data) => success: (data) =>
feature_res = data.feature_results[feature] feature_res = data.feature_results
# feature response format: {'error': 'optional error string', 'type': 'SOME_TYPE', 'data': [stuff]} if feature_res.type is 'EASY_CHOICE'
if feature_res.error # display on SlickGrid
console.warn(feature_res.error) options =
@$display_text.text 'Error fetching data' enableCellNavigation: true
enableColumnReorder: false
forceFitColumns: true
columns = [
id: feature
field: feature
name: feature
,
id: 'count'
field: 'count'
name: 'Count'
]
grid_data = _.map feature_res.data, (value, key) ->
datapoint = {}
datapoint[feature] = feature_res.choices_display_names[key]
datapoint['count'] = value
datapoint
table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
else if feature_res.feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth'
@$display_graph.append graph_placeholder
graph_data = _.map feature_res.data, (value, key) -> [parseInt(key), value]
$.plot graph_placeholder, [
data: graph_data
]
else else
if feature_res.type is 'EASY_CHOICE' console.warn("unable to show distribution #{feature_res.type}")
# display on SlickGrid @$display_text.text 'Unavailable Metric Display\n' + JSON.stringify(feature_res)
options =
enableCellNavigation: true
enableColumnReorder: false
forceFitColumns: true
columns = [
id: feature
field: feature
name: feature
,
id: 'count'
field: 'count'
name: 'Count'
]
grid_data = _.map feature_res.data, (value, key) ->
datapoint = {}
datapoint[feature] = feature_res.display_names[key]
datapoint['count'] = value
datapoint
table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
else if feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth'
@$display_graph.append graph_placeholder
graph_data = _.map feature_res.data, (value, key) -> [parseInt(key), value]
$.plot graph_placeholder, [
data: graph_data
]
else
console.warn("don't know how to show #{feature_res.type}")
@$display_text.text 'Unavailable Metric\n' + JSON.stringify(feature_res)
# fetch distribution data from server. # fetch distribution data from server.
# `handler` can be either a callback for success # `handler` can be either a callback for success
# or a mapping e.g. {success: ->, error: ->, complete: ->} # or a mapping e.g. {success: ->, error: ->, complete: ->}
get_profile_distributions: (featurelist, handler) -> get_profile_distributions: (feature, handler) ->
settings = settings =
dataType: 'json' dataType: 'json'
url: @$distribution_select.data 'endpoint' url: @$distribution_select.data 'endpoint'
data: features: JSON.stringify featurelist data: feature: feature
if typeof handler is 'function' if typeof handler is 'function'
_.extend settings, success: handler _.extend settings, success: handler
......
...@@ -36,9 +36,7 @@ HASH_LINK_PREFIX = '#view-' ...@@ -36,9 +36,7 @@ HASH_LINK_PREFIX = '#view-'
# once we're ready, check if this page is the instructor dashboard # once we're ready, check if this page is the instructor dashboard
$ => $ =>
instructor_dashboard_content = $ ".#{CSS_INSTRUCTOR_CONTENT}" instructor_dashboard_content = $ ".#{CSS_INSTRUCTOR_CONTENT}"
console.log 'checking if we are on the instructor dashboard'
if instructor_dashboard_content.length > 0 if instructor_dashboard_content.length > 0
console.log 'we are on the instructor dashboard'
setup_instructor_dashboard instructor_dashboard_content setup_instructor_dashboard instructor_dashboard_content
setup_instructor_dashboard_sections instructor_dashboard_content setup_instructor_dashboard_sections instructor_dashboard_content
......
<%page args="section_data"/> <%page args="section_data"/>
<h2>Distributions</h2> <h2>Distributions</h2>
<select id="distributions" data-endpoint="${ section_data['profile_distributions_url'] }"> <select id="distributions" data-endpoint="${ section_data['get_distribution_url'] }">
<option> Getting available distributions... </option> <option> Getting available distributions... </option>
</select> </select>
<div class="distribution-display"> <div class="distribution-display">
......
<%page args="section_data"/> <%page args="section_data"/>
<input type="button" name="list-profiles" value="List enrolled students with profile information" data-endpoint="${ section_data['enrolled_students_features_url'] }" > <input type="button" name="list-profiles" value="List enrolled students with profile information" data-endpoint="${ section_data['get_students_features_url'] }" >
<input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv" data-endpoint="${ section_data['enrolled_students_features_url'] }" > <input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv" data-endpoint="${ section_data['get_students_features_url'] }" >
<br> <br>
## <input type="button" name="list-grades" value="Student grades"> ## <input type="button" name="list-grades" value="Student grades">
## <input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv"> ## <input type="button" name="list-profiles" value="CSV" data-csv="true" class="csv">
## <br> ## <br>
## <input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)"> ## <input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)">
## <br> ## <br>
<input type="button" name="dump-gradeconf" value="Grading Configuration" data-endpoint="${ section_data['grading_config_url'] }"> <input type="button" name="dump-gradeconf" value="Grading Configuration" data-endpoint="${ section_data['get_grading_config_url'] }">
<div class="data-display"> <div class="data-display">
<div class="data-display-text"></div> <div class="data-display-text"></div>
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
%if section_data['access']['instructor']: %if section_data['access']['instructor']:
<div class="auth-list-container" data-rolename="staff" data-display-name="Staff"> <div class="auth-list-container" data-rolename="staff" data-display-name="Staff">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_url'] }"></div> <div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_url'] }"></div>
<div class="auth-list-add" data-endpoint="${ section_data['access_allow_revoke_url'] }"> <div class="auth-list-add" data-endpoint="${ section_data['modify_access_url'] }">
<input type="text" name="email" placeholder="Enter Email" spellcheck="false"> <input type="text" name="email" placeholder="Enter Email" spellcheck="false">
<input type="button" name="allow" value="Grant Staff Access"> <input type="button" name="allow" value="Grant Staff Access">
</div> </div>
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
%if section_data['access']['instructor']: %if section_data['access']['instructor']:
<div class="auth-list-container" data-rolename="instructor" data-display-name="Instructors"> <div class="auth-list-container" data-rolename="instructor" data-display-name="Instructors">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_url'] }"></div> <div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_url'] }"></div>
<div class="auth-list-add" data-endpoint="${ section_data['access_allow_revoke_url'] }"> <div class="auth-list-add" data-endpoint="${ section_data['modify_access_url'] }">
<input type="text" name="email" placeholder="Enter Email" spellcheck="false"> <input type="text" name="email" placeholder="Enter Email" spellcheck="false">
<input type="button" name="allow" value="Grant Instructor Access"> <input type="button" name="allow" value="Grant Instructor Access">
</div> </div>
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
<div class="auth-list-container" data-rolename="beta" data-display-name="Beta Testers"> <div class="auth-list-container" data-rolename="beta" data-display-name="Beta Testers">
<div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_url'] }"></div> <div class="auth-list-table" data-endpoint="${ section_data['list_course_role_members_url'] }"></div>
<div class="auth-list-add" data-endpoint="${ section_data['access_allow_revoke_url'] }"> <div class="auth-list-add" data-endpoint="${ section_data['modify_access_url'] }">
<input type="text" name="email" placeholder="Enter Email" spellcheck="false"> <input type="text" name="email" placeholder="Enter Email" spellcheck="false">
<input type="button" name="allow" value="Grant Beta Tester Access"> <input type="button" name="allow" value="Grant Beta Tester Access">
</div> </div>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<br> <br>
<div class="progress-link-wrapper"> <div class="progress-link-wrapper">
<a href="" class="progress-link" data-endpoint="${ section_data['get_student_progress_url'] }">Student Progress Page</a> <a href="" class="progress-link" data-endpoint="${ section_data['get_student_progress_url_url'] }">Student Progress Page</a>
</div> </div>
<br> <br>
......
...@@ -350,14 +350,14 @@ if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR ...@@ -350,14 +350,14 @@ if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR
'instructor.views.api.students_update_enrollment', name="students_update_enrollment"), 'instructor.views.api.students_update_enrollment', name="students_update_enrollment"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_course_role_members$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/list_course_role_members$',
'instructor.views.api.list_course_role_members', name="list_course_role_members"), 'instructor.views.api.list_course_role_members', name="list_course_role_members"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/access_allow_revoke$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/modify_access$',
'instructor.views.api.access_allow_revoke', name="access_allow_revoke"), 'instructor.views.api.modify_access', name="modify_access"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/grading_config$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/get_grading_config$',
'instructor.views.api.grading_config', name="grading_config"), 'instructor.views.api.get_grading_config', name="get_grading_config"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/enrolled_students_features(?P<csv>/csv)?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/get_students_features(?P<csv>/csv)?$',
'instructor.views.api.enrolled_students_features', name="enrolled_students_features"), 'instructor.views.api.get_students_features', name="get_students_features"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/profile_distribution$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/get_distribution$',
'instructor.views.api.profile_distribution', name="profile_distribution"), 'instructor.views.api.get_distribution', name="get_distribution"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/get_student_progress_url$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/get_student_progress_url$',
'instructor.views.api.get_student_progress_url', name="get_student_progress_url"), 'instructor.views.api.get_student_progress_url', name="get_student_progress_url"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/reset_student_attempts$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/reset_student_attempts$',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment