Merge pull request #7322 from edx/sarina/remove-old-analytics-dashboard
Remove everything gated by ENABLE_INSTRUCTOR_ANALYTICS flag AN-4583
This commit is contained in:
@@ -70,7 +70,6 @@
|
||||
"CUSTOM_COURSE_URLS": true
|
||||
},
|
||||
"ENABLE_DISCUSSION_SERVICE": true,
|
||||
"ENABLE_INSTRUCTOR_ANALYTICS": true,
|
||||
"ENABLE_S3_GRADE_DOWNLOADS": true,
|
||||
"ENTRANCE_EXAMS": true,
|
||||
"MILESTONES_APP": true,
|
||||
|
||||
@@ -61,8 +61,7 @@ import instructor_task.api
|
||||
import instructor.views.api
|
||||
from instructor.views.api import require_finance_admin
|
||||
from instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo
|
||||
from instructor.views.api import generate_unique_password
|
||||
from instructor.views.api import _split_input_list, common_exceptions_400
|
||||
from instructor.views.api import _split_input_list, common_exceptions_400, generate_unique_password
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
|
||||
@@ -208,14 +207,12 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
{'identifiers': 'foo@example.org', 'action': 'enroll'}),
|
||||
('get_grading_config', {}),
|
||||
('get_students_features', {}),
|
||||
('get_distribution', {}),
|
||||
('get_student_progress_url', {'unique_student_identifier': self.user.username}),
|
||||
('reset_student_attempts',
|
||||
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
|
||||
('update_forum_role_membership',
|
||||
{'unique_student_identifier': self.user.email, 'rolename': 'Moderator', 'action': 'allow'}),
|
||||
('list_forum_members', {'rolename': FORUM_ROLE_COMMUNITY_TA}),
|
||||
('proxy_legacy_analytics', {'aname': 'ProblemGradeDistribution'}),
|
||||
('send_email', {'send_to': 'staff', 'subject': 'test', 'message': 'asdf'}),
|
||||
('list_instructor_tasks', {}),
|
||||
('list_background_email_tasks', {}),
|
||||
@@ -291,7 +288,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
for endpoint, args in self.staff_level_endpoints:
|
||||
# TODO: make these work
|
||||
if endpoint in ['update_forum_role_membership', 'proxy_legacy_analytics', 'list_forum_members']:
|
||||
if endpoint in ['update_forum_role_membership', 'list_forum_members']:
|
||||
continue
|
||||
self._access_endpoint(
|
||||
endpoint,
|
||||
@@ -320,7 +317,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
for endpoint, args in self.staff_level_endpoints:
|
||||
# TODO: make these work
|
||||
if endpoint in ['update_forum_role_membership', 'proxy_legacy_analytics']:
|
||||
if endpoint in ['update_forum_role_membership']:
|
||||
continue
|
||||
self._access_endpoint(
|
||||
endpoint,
|
||||
@@ -1978,7 +1975,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
percentage_discount=10, created_by=self.instructor, is_active=True)
|
||||
self.coupon.save()
|
||||
|
||||
#create testing invoice 1
|
||||
# Create testing invoice 1
|
||||
self.sale_invoice_1 = Invoice.objects.create(
|
||||
total_amount=1234.32, company_name='Test1', company_contact_name='TestName', company_contact_email='Test@company.com',
|
||||
recipient_name='Testw', recipient_email='test1@test.com', customer_reference_number='2Fwe23S',
|
||||
@@ -2205,7 +2202,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
#create test invoice 2
|
||||
# Create test invoice 2
|
||||
sale_invoice_2 = Invoice.objects.create(
|
||||
total_amount=1234.32, company_name='Test1', company_contact_name='TestName', company_contact_email='Test@company.com',
|
||||
recipient_name='Testw_2', recipient_email='test2@test.com', customer_reference_number='2Fwe23S',
|
||||
@@ -2602,46 +2599,6 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
" report when it is complete.".format(report_type=report_type)
|
||||
self.assertIn(already_running_status, response.content)
|
||||
|
||||
def test_get_distribution_no_feature(self):
|
||||
"""
|
||||
Test that get_distribution lists available features
|
||||
when supplied no feature parameter.
|
||||
"""
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
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.to_deprecated_string()})
|
||||
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.to_deprecated_string()})
|
||||
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.to_deprecated_string()})
|
||||
response = self.client.get(url, {'feature': 'gender'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
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.to_deprecated_string()})
|
||||
@@ -3459,155 +3416,6 @@ class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCas
|
||||
self.assertDictEqual(expected_info, returned_info)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
@override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/")
|
||||
@override_settings(ANALYTICS_API_KEY="robot_api_key")
|
||||
class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Test instructor analytics proxy endpoint.
|
||||
"""
|
||||
|
||||
class FakeProxyResponse(object):
|
||||
""" Fake successful requests response object. """
|
||||
|
||||
def __init__(self):
|
||||
self.status_code = requests.status_codes.codes.OK
|
||||
self.content = '{"test_content": "robot test content"}'
|
||||
|
||||
class FakeBadProxyResponse(object):
|
||||
""" Fake strange-failed requests response object. """
|
||||
|
||||
def __init__(self):
|
||||
self.status_code = 'notok.'
|
||||
self.content = '{"test_content": "robot test content"}'
|
||||
|
||||
def setUp(self):
|
||||
super(TestInstructorAPIAnalyticsProxy, self).setUp()
|
||||
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, False), (ModuleStoreEnum.Type.split, True))
|
||||
@ddt.unpack
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy_url(self, store_type, assert_wo_encoding, act):
|
||||
""" Test legacy analytics proxy url generation. """
|
||||
with modulestore().default_store(store_type):
|
||||
course = CourseFactory.create()
|
||||
instructor_local = InstructorFactory(course_key=course.id)
|
||||
self.client.login(username=instructor_local.username, password='test')
|
||||
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make request URL pattern - everything but course id.
|
||||
url_pattern = "{url}get?aname={aname}&course_id={course_id}&apikey={api_key}".format(
|
||||
url="http://robotanalyticsserver.netbot:900/",
|
||||
aname="ProblemGradeDistribution",
|
||||
course_id="{course_id!s}",
|
||||
api_key="robot_api_key",
|
||||
)
|
||||
|
||||
if assert_wo_encoding:
|
||||
# Format url with no URL-encoding of parameters.
|
||||
assert_url = url_pattern.format(course_id=course.id.to_deprecated_string())
|
||||
with self.assertRaises(AssertionError):
|
||||
act.assert_called_once_with(assert_url)
|
||||
|
||||
# Format url *with* URL-encoding of parameters.
|
||||
expected_url = url_pattern.format(course_id=quote(course.id.to_deprecated_string()))
|
||||
act.assert_called_once_with(expected_url)
|
||||
|
||||
@override_settings(ANALYTICS_SERVER_URL="")
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy_server_url(self, act):
|
||||
"""
|
||||
Test legacy analytics when empty server url.
|
||||
"""
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
self.assertEqual(response.status_code, 501)
|
||||
|
||||
@override_settings(ANALYTICS_API_KEY="")
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy_api_key(self, act):
|
||||
"""
|
||||
Test legacy analytics when empty server API key.
|
||||
"""
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
self.assertEqual(response.status_code, 501)
|
||||
|
||||
@override_settings(ANALYTICS_SERVER_URL="")
|
||||
@override_settings(ANALYTICS_API_KEY="")
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy_empty_url_and_api_key(self, act):
|
||||
"""
|
||||
Test legacy analytics when empty server url & API key.
|
||||
"""
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
self.assertEqual(response.status_code, 501)
|
||||
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy(self, act):
|
||||
"""
|
||||
Test legacy analytics content proxyin, actg.
|
||||
"""
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# check response
|
||||
self.assertTrue(act.called)
|
||||
expected_res = {'test_content': "robot test content"}
|
||||
self.assertEqual(json.loads(response.content), expected_res)
|
||||
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy_reqfailed(self, act):
|
||||
""" Test proxy when server reponds with failure. """
|
||||
act.return_value = self.FakeBadProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
def test_analytics_proxy_missing_param(self, act):
|
||||
""" Test proxy when missing the aname query parameter. """
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(act.called)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestInstructorAPIHelpers(TestCase):
|
||||
""" Test helpers for instructor.api """
|
||||
|
||||
@@ -54,11 +54,11 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
return 'Enrollment data is now available in <a href="http://example.com/courses/{}" ' \
|
||||
'target="_blank">Example</a>.'.format(unicode(self.course.id))
|
||||
|
||||
def get_dashboard_demographic_message(self):
|
||||
def get_dashboard_analytics_message(self):
|
||||
"""
|
||||
Returns expected dashboard demographic message with link to Insights.
|
||||
"""
|
||||
return 'Demographic data is now available in <a href="http://example.com/courses/{}" ' \
|
||||
return 'For analytics about your course, go to <a href="http://example.com/courses/{}" ' \
|
||||
'target="_blank">Example</a>.'.format(unicode(self.course.id))
|
||||
|
||||
def test_instructor_tab(self):
|
||||
@@ -157,38 +157,28 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
expected_message = self.get_dashboard_enrollment_message()
|
||||
self.assertTrue(expected_message in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_DEMOGRAPHICS': True})
|
||||
@override_settings(ANALYTICS_DASHBOARD_URL='')
|
||||
@override_settings(ANALYTICS_DASHBOARD_NAME='')
|
||||
def test_show_dashboard_demographic_data(self):
|
||||
def test_dashboard_analytics_tab_not_shown(self):
|
||||
"""
|
||||
Test enrollment demographic data is shown.
|
||||
Test dashboard analytics tab isn't shown if insights isn't configured.
|
||||
"""
|
||||
response = self.client.get(self.url)
|
||||
# demographic information displayed
|
||||
self.assertTrue('data-feature="year_of_birth"' in response.content)
|
||||
self.assertTrue('data-feature="gender"' in response.content)
|
||||
self.assertTrue('data-feature="level_of_education"' in response.content)
|
||||
analytics_section = '<li class="nav-item"><a href="" data-section="instructor_analytics">Analytics</a></li>'
|
||||
self.assertFalse(analytics_section in response.content)
|
||||
|
||||
# dashboard link hidden
|
||||
self.assertFalse(self.get_dashboard_demographic_message() in response.content)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_DEMOGRAPHICS': False})
|
||||
@override_settings(ANALYTICS_DASHBOARD_URL='http://example.com')
|
||||
@override_settings(ANALYTICS_DASHBOARD_NAME='Example')
|
||||
def test_show_dashboard_demographic_message(self):
|
||||
def test_dashboard_analytics_points_at_insights(self):
|
||||
"""
|
||||
Test enrollment demographic dashboard message is shown and data is hidden.
|
||||
Test analytics dashboard message is shown
|
||||
"""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# demographics are hidden
|
||||
self.assertFalse('data-feature="year_of_birth"' in response.content)
|
||||
self.assertFalse('data-feature="gender"' in response.content)
|
||||
self.assertFalse('data-feature="level_of_education"' in response.content)
|
||||
analytics_section = '<li class="nav-item"><a href="" data-section="instructor_analytics">Analytics</a></li>'
|
||||
self.assertTrue(analytics_section in response.content)
|
||||
|
||||
# link to dashboard shown
|
||||
expected_message = self.get_dashboard_demographic_message()
|
||||
expected_message = self.get_dashboard_analytics_message()
|
||||
self.assertTrue(expected_message in response.content)
|
||||
|
||||
def add_course_to_user_cart(self, cart, course_key):
|
||||
|
||||
@@ -1645,56 +1645,6 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument
|
||||
return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
def get_distribution(request, course_id):
|
||||
"""
|
||||
Respond with json of the distribution of students over selected features which have choices.
|
||||
|
||||
Ask for a feature through the `feature` query parameter.
|
||||
If no `feature` is supplied, will return response with an
|
||||
empty response['feature_results'] object.
|
||||
A list of available will be available in the response['available_features']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
feature = request.GET.get('feature')
|
||||
# alternate notations of None
|
||||
if feature in (None, 'null', ''):
|
||||
feature = None
|
||||
else:
|
||||
feature = str(feature)
|
||||
|
||||
available_features = instructor_analytics.distributions.AVAILABLE_PROFILE_FEATURES
|
||||
# allow None so that requests for no feature can list available features
|
||||
if feature not in available_features + (None,):
|
||||
return HttpResponseBadRequest(strip_tags(
|
||||
"feature '{}' not available.".format(feature)
|
||||
))
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'queried_feature': feature,
|
||||
'available_features': available_features,
|
||||
'feature_display_names': instructor_analytics.distributions.DISPLAY_NAMES,
|
||||
}
|
||||
|
||||
p_dist = None
|
||||
if feature is not None:
|
||||
p_dist = instructor_analytics.distributions.profile_distribution(course_id, feature)
|
||||
response_payload['feature_results'] = {
|
||||
'feature': p_dist.feature,
|
||||
'feature_display_name': p_dist.feature_display_name,
|
||||
'data': p_dist.data,
|
||||
'type': p_dist.type,
|
||||
}
|
||||
|
||||
if p_dist.type == 'EASY_CHOICE':
|
||||
response_payload['feature_results']['choices_display_names'] = p_dist.choices_display_names
|
||||
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@common_exceptions_400
|
||||
@@ -2361,62 +2311,6 @@ def update_forum_role_membership(request, course_id):
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_query_params(
|
||||
aname="name of analytic to query",
|
||||
)
|
||||
@common_exceptions_400
|
||||
def proxy_legacy_analytics(request, course_id):
|
||||
"""
|
||||
Proxies to the analytics cron job server.
|
||||
|
||||
`aname` is a query parameter specifying which analytic to query.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
analytics_name = request.GET.get('aname')
|
||||
|
||||
# abort if misconfigured
|
||||
if not (hasattr(settings, 'ANALYTICS_SERVER_URL') and
|
||||
hasattr(settings, 'ANALYTICS_API_KEY') and
|
||||
settings.ANALYTICS_SERVER_URL and settings.ANALYTICS_API_KEY):
|
||||
return HttpResponse("Analytics service not configured.", status=501)
|
||||
|
||||
url = "{}get?aname={}&course_id={}&apikey={}".format(
|
||||
settings.ANALYTICS_SERVER_URL,
|
||||
analytics_name,
|
||||
urllib.quote(unicode(course_id)),
|
||||
settings.ANALYTICS_API_KEY,
|
||||
)
|
||||
|
||||
try:
|
||||
res = requests.get(url)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception(u"Error requesting from analytics server at %s", url)
|
||||
return HttpResponse("Error requesting from analytics server.", status=500)
|
||||
|
||||
if res.status_code is 200:
|
||||
payload = json.loads(res.content)
|
||||
add_block_ids(payload)
|
||||
content = json.dumps(payload)
|
||||
# return the successful request content
|
||||
return HttpResponse(content, content_type="application/json")
|
||||
elif res.status_code is 404:
|
||||
# forward the 404 and content
|
||||
return HttpResponse(res.content, content_type="application/json", status=404)
|
||||
else:
|
||||
# 500 on all other unexpected status codes.
|
||||
log.error(
|
||||
u"Error fetching %s, code: %s, msg: %s",
|
||||
url, res.status_code, res.content
|
||||
)
|
||||
return HttpResponse(
|
||||
"Error from analytics server ({}).".format(res.status_code),
|
||||
status=500
|
||||
)
|
||||
|
||||
|
||||
@require_POST
|
||||
def get_user_invoice_preference(request, course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
|
||||
@@ -33,8 +33,6 @@ urlpatterns = patterns(
|
||||
'instructor.views.api.sale_validation', name="sale_validation"),
|
||||
url(r'^get_anon_ids$',
|
||||
'instructor.views.api.get_anon_ids', name="get_anon_ids"),
|
||||
url(r'^get_distribution$',
|
||||
'instructor.views.api.get_distribution', name="get_distribution"),
|
||||
url(r'^get_student_progress_url$',
|
||||
'instructor.views.api.get_student_progress_url', name="get_student_progress_url"),
|
||||
url(r'^reset_student_attempts$',
|
||||
@@ -71,8 +69,6 @@ urlpatterns = patterns(
|
||||
'instructor.views.api.list_forum_members', name="list_forum_members"),
|
||||
url(r'^update_forum_role_membership$',
|
||||
'instructor.views.api.update_forum_role_membership', name="update_forum_role_membership"),
|
||||
url(r'^proxy_legacy_analytics$',
|
||||
'instructor.views.api.proxy_legacy_analytics', name="proxy_legacy_analytics"),
|
||||
url(r'^send_email$',
|
||||
'instructor.views.api.send_email', name="send_email"),
|
||||
url(r'^change_due_date$', 'instructor.views.api.change_due_date',
|
||||
|
||||
@@ -97,10 +97,24 @@ def instructor_dashboard_2(request, course_id):
|
||||
_section_cohort_management(course, access),
|
||||
_section_student_admin(course, access),
|
||||
_section_data_download(course, access),
|
||||
_section_analytics(course, access),
|
||||
]
|
||||
|
||||
#check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
|
||||
analytics_dashboard_message = None
|
||||
if settings.ANALYTICS_DASHBOARD_URL:
|
||||
# Construct a URL to the external analytics dashboard
|
||||
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
|
||||
link_start = "<a href=\"{}\" target=\"_blank\">".format(analytics_dashboard_url)
|
||||
analytics_dashboard_message = _(
|
||||
"To gain insights into student enrollment and participation {link_start}"
|
||||
"visit {analytics_dashboard_name}, our new course analytics product{link_end}."
|
||||
)
|
||||
analytics_dashboard_message = analytics_dashboard_message.format(
|
||||
link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)
|
||||
|
||||
# Temporarily show the "Analytics" section until we have a better way of linking to Insights
|
||||
sections.append(_section_analytics(course, access))
|
||||
|
||||
# Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
|
||||
course_mode_has_price = False
|
||||
paid_modes = CourseMode.paid_modes_for_course(course_key)
|
||||
if len(paid_modes) == 1:
|
||||
@@ -136,15 +150,6 @@ def instructor_dashboard_2(request, course_id):
|
||||
|
||||
disable_buttons = not _is_small_course(course_key)
|
||||
|
||||
analytics_dashboard_message = None
|
||||
if settings.ANALYTICS_DASHBOARD_URL:
|
||||
# Construct a URL to the external analytics dashboard
|
||||
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
|
||||
link_start = "<a href=\"{}\" target=\"_blank\">".format(analytics_dashboard_url)
|
||||
analytics_dashboard_message = _("To gain insights into student enrollment and participation {link_start}visit {analytics_dashboard_name}, our new course analytics product{link_end}.")
|
||||
analytics_dashboard_message = analytics_dashboard_message.format(
|
||||
link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}),
|
||||
@@ -530,19 +535,20 @@ def _get_dashboard_link(course_key):
|
||||
def _section_analytics(course, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
course_key = course.id
|
||||
analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
|
||||
link_start = "<a href=\"{}\" target=\"_blank\">".format(analytics_dashboard_url)
|
||||
insights_message = _("For analytics about your course, go to {analytics_dashboard_name}.")
|
||||
|
||||
insights_message = insights_message.format(
|
||||
analytics_dashboard_name='{0}{1}</a>'.format(link_start, settings.ANALYTICS_DASHBOARD_NAME)
|
||||
)
|
||||
section_data = {
|
||||
'section_key': 'instructor_analytics',
|
||||
'section_display_name': _('Analytics'),
|
||||
'access': access,
|
||||
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': unicode(course_key)}),
|
||||
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': unicode(course_key)}),
|
||||
'insights_message': insights_message,
|
||||
}
|
||||
|
||||
if settings.ANALYTICS_DASHBOARD_URL:
|
||||
dashboard_link = _get_dashboard_link(course_key)
|
||||
message = _("Demographic data is now available in {dashboard_link}.").format(dashboard_link=dashboard_link)
|
||||
section_data['demographic_message'] = message
|
||||
|
||||
return section_data
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@
|
||||
"ENABLE_PAYMENT_FAKE": true,
|
||||
"ENABLE_VERIFIED_CERTIFICATES": true,
|
||||
"ENABLE_DISCUSSION_SERVICE": true,
|
||||
"ENABLE_INSTRUCTOR_ANALYTICS": true,
|
||||
"ENABLE_S3_GRADE_DOWNLOADS": true,
|
||||
"ENABLE_THIRD_PARTY_AUTH": true,
|
||||
"ENABLE_COMBINED_LOGIN_REGISTRATION": true,
|
||||
|
||||
@@ -169,10 +169,6 @@ FEATURES = {
|
||||
# for all Mongo-backed courses.
|
||||
'REQUIRE_COURSE_EMAIL_AUTH': True,
|
||||
|
||||
# Analytics experiments - shows instructor analytics tab in LMS instructor dashboard.
|
||||
# Enabling this feature depends on installation of a separate analytics server.
|
||||
'ENABLE_INSTRUCTOR_ANALYTICS': False,
|
||||
|
||||
# enable analytics server.
|
||||
# WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL
|
||||
# LMS OPERATION. See analytics.py for details about what
|
||||
@@ -338,10 +334,7 @@ FEATURES = {
|
||||
# and register for course.
|
||||
'ALLOW_AUTOMATED_SIGNUPS': False,
|
||||
|
||||
# Display demographic data on the analytics tab in the instructor dashboard.
|
||||
'DISPLAY_ANALYTICS_DEMOGRAPHICS': True,
|
||||
|
||||
# Enable display of enrollment counts in instructor and legacy analytics dashboard
|
||||
# Enable display of enrollment counts in instructor dash, analytics section
|
||||
'DISPLAY_ANALYTICS_ENROLLMENTS': True,
|
||||
|
||||
# Show the mobile app links in the footer
|
||||
|
||||
@@ -30,7 +30,6 @@ FEATURES['SUBDOMAIN_BRANDING'] = True
|
||||
FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST)
|
||||
FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
|
||||
FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
|
||||
FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
|
||||
FEATURES['ENABLE_SERVICE_STATUS'] = True
|
||||
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
|
||||
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
###
|
||||
Analytics Section
|
||||
|
||||
imports from other modules.
|
||||
wrap in (-> ... apply) to defer evaluation
|
||||
such that the value can be defined later than this assignment (file load order).
|
||||
###
|
||||
|
||||
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
|
||||
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
|
||||
|
||||
|
||||
class ProfileDistributionWidget
|
||||
constructor: ({@$container, @feature, @title, @endpoint}) ->
|
||||
# render template
|
||||
template_params =
|
||||
title: @title
|
||||
feature: @feature
|
||||
endpoint: @endpoint
|
||||
template_html = $("#profile-distribution-widget-template").text()
|
||||
@$container.html Mustache.render template_html, template_params
|
||||
|
||||
reset_display: ->
|
||||
@$container.find('.display-errors').empty()
|
||||
@$container.find('.display-text').empty()
|
||||
@$container.find('.display-graph').empty()
|
||||
@$container.find('.display-table').empty()
|
||||
|
||||
show_error: (msg) ->
|
||||
@$container.find('.display-errors').text msg
|
||||
|
||||
# display data
|
||||
load: ->
|
||||
@reset_display()
|
||||
|
||||
@get_profile_distributions @feature,
|
||||
error: std_ajax_err =>
|
||||
`// Translators: "Distribution" refers to a grade distribution. This error message appears when there is an error getting the data on grade distribution.`
|
||||
@show_error gettext("Error fetching distribution.")
|
||||
success: (data) =>
|
||||
feature_res = data.feature_results
|
||||
if feature_res.type is 'EASY_CHOICE'
|
||||
# display on SlickGrid
|
||||
options =
|
||||
enableCellNavigation: true
|
||||
enableColumnReorder: false
|
||||
forceFitColumns: true
|
||||
|
||||
columns = [
|
||||
id: @feature
|
||||
field: @feature
|
||||
name: data.feature_display_names[@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'
|
||||
@$container.find('.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: 'graph-placeholder'
|
||||
@$container.find('.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("unable to show distribution #{feature_res.type}")
|
||||
@show_error gettext('Unavailable metric display.')
|
||||
|
||||
# fetch distribution data from server.
|
||||
# `handler` can be either a callback for success
|
||||
# or a mapping e.g. {success: ->, error: ->, complete: ->}
|
||||
get_profile_distributions: (feature, handler) ->
|
||||
settings =
|
||||
dataType: 'json'
|
||||
url: @endpoint
|
||||
data: feature: feature
|
||||
|
||||
if typeof handler is 'function'
|
||||
_.extend settings, success: handler
|
||||
else
|
||||
_.extend settings, handler
|
||||
|
||||
$.ajax settings
|
||||
|
||||
|
||||
class GradeDistributionDisplay
|
||||
constructor: ({@$container, @endpoint}) ->
|
||||
template_params = {}
|
||||
template_html = $('#grade-distributions-widget-template').text()
|
||||
@$container.html Mustache.render template_html, template_params
|
||||
@$problem_selector = @$container.find '.problem-selector'
|
||||
|
||||
reset_display: ->
|
||||
@$container.find('.display-errors').empty()
|
||||
@$container.find('.display-text').empty()
|
||||
@$container.find('.display-graph').empty()
|
||||
|
||||
show_error: (msg) ->
|
||||
@$container.find('.display-errors').text msg
|
||||
|
||||
load: ->
|
||||
@get_grade_distributions
|
||||
error: std_ajax_err => @show_error gettext("Error fetching grade distributions.")
|
||||
success: (data) =>
|
||||
time_updated = gettext("Last Updated: <%= timestamp %>")
|
||||
full_time_updated = _.template(time_updated, {timestamp: data.time})
|
||||
@$container.find('.last-updated').text full_time_updated
|
||||
|
||||
# populate selector
|
||||
@$problem_selector.empty()
|
||||
for {module_id, block_id, grade_info} in data.data
|
||||
label = block_id
|
||||
label ?= module_id
|
||||
|
||||
@$problem_selector.append $ '<option/>',
|
||||
text: label
|
||||
data:
|
||||
module_id: module_id
|
||||
grade_info: grade_info
|
||||
|
||||
@$problem_selector.change =>
|
||||
$opt = @$problem_selector.children('option:selected')
|
||||
return unless $opt.length > 0
|
||||
@reset_display()
|
||||
@render_distribution
|
||||
module_id: $opt.data 'module_id'
|
||||
grade_info: $opt.data 'grade_info'
|
||||
|
||||
# one-time first selection of first list item.
|
||||
@$problem_selector.change()
|
||||
|
||||
render_distribution: ({module_id, grade_info}) ->
|
||||
$display_graph = @$container.find('.display-graph')
|
||||
|
||||
graph_data = grade_info.map ({grade, max_grade, num_students}) -> [grade, num_students]
|
||||
total_students = _.reduce ([0].concat grade_info),
|
||||
(accum, {grade, max_grade, num_students}) -> accum + num_students
|
||||
|
||||
msg = gettext("<%= num_students %> students scored.")
|
||||
full_msg = _.template(msg, {num_students: total_students})
|
||||
# show total students
|
||||
@$container.find('.display-text').text full_msg
|
||||
|
||||
# render to graph
|
||||
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
|
||||
$display_graph.append graph_placeholder
|
||||
|
||||
graph_data = graph_data
|
||||
|
||||
$.plot graph_placeholder, [
|
||||
data: graph_data
|
||||
bars: show: true
|
||||
color: '#1d9dd9'
|
||||
]
|
||||
|
||||
|
||||
# `handler` can be either a callback for success
|
||||
# or a mapping e.g. {success: ->, error: ->, complete: ->}
|
||||
#
|
||||
# the data passed to the success handler takes this form:
|
||||
# {
|
||||
# "aname": "ProblemGradeDistribution",
|
||||
# "time": "2013-07-31T20:25:56+00:00",
|
||||
# "course_id": "MITx/6.002x/2013_Spring",
|
||||
# "options": {
|
||||
# "course_id": "MITx/6.002x/2013_Spring",
|
||||
# "_id": "6fudge2b49somedbid1e1",
|
||||
# "data": [
|
||||
# {
|
||||
# "module_id": "i4x://MITx/6.002x/problem/Capacitors_and_Energy_Storage",
|
||||
# "grade_info": [
|
||||
# {
|
||||
# "grade": 0.0,
|
||||
# "max_grade": 100.0,
|
||||
# "num_students": 3
|
||||
# }, ... for each grade number between 0 and max_grade
|
||||
# ],
|
||||
# }
|
||||
get_grade_distributions: (handler) ->
|
||||
settings =
|
||||
dataType: 'json'
|
||||
url: @endpoint
|
||||
data: aname: 'ProblemGradeDistribution'
|
||||
|
||||
if typeof handler is 'function'
|
||||
_.extend settings, success: handler
|
||||
else
|
||||
_.extend settings, handler
|
||||
|
||||
$.ajax settings
|
||||
|
||||
|
||||
# Analytics Section
|
||||
class InstructorAnalytics
|
||||
constructor: (@$section) ->
|
||||
@$section.data 'wrapper', @
|
||||
|
||||
@$pd_containers = @$section.find '.profile-distribution-widget-container'
|
||||
@$gd_containers = @$section.find '.grade-distributions-widget-container'
|
||||
|
||||
@pdws = _.map (@$pd_containers), (container) =>
|
||||
new ProfileDistributionWidget
|
||||
$container: $(container)
|
||||
feature: $(container).data 'feature'
|
||||
title: $(container).data 'title'
|
||||
endpoint: $(container).data 'endpoint'
|
||||
|
||||
@gdws = _.map (@$gd_containers), (container) =>
|
||||
new GradeDistributionDisplay
|
||||
$container: $(container)
|
||||
endpoint: $(container).data 'endpoint'
|
||||
|
||||
refresh: ->
|
||||
for pdw in @pdws
|
||||
pdw.load()
|
||||
|
||||
for gdw in @gdws
|
||||
gdw.load()
|
||||
|
||||
onClickTitle: ->
|
||||
@refresh()
|
||||
|
||||
|
||||
# export for use
|
||||
# create parent namespaces if they do not already exist.
|
||||
_.defaults window, InstructorDashboard: {}
|
||||
_.defaults window.InstructorDashboard, sections: {}
|
||||
_.defaults window.InstructorDashboard.sections,
|
||||
InstructorAnalytics: InstructorAnalytics
|
||||
@@ -1355,40 +1355,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.profile-distribution-widget {
|
||||
margin-bottom: ($baseline * 2);
|
||||
|
||||
.display-graph .graph-placeholder {
|
||||
width: 750px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.display-table {
|
||||
.slickgrid {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grade-distributions-widget {
|
||||
margin-bottom: $baseline * 2;
|
||||
|
||||
.last-updated {
|
||||
line-height: 2.2em;
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.display-graph .graph-placeholder {
|
||||
width: 750px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.display-text {
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
input[name="subject"] {
|
||||
width:600px;
|
||||
}
|
||||
@@ -1875,39 +1841,6 @@ input[name="subject"] {
|
||||
|
||||
}
|
||||
|
||||
.profile-distribution-widget {
|
||||
margin-bottom: ($baseline * 2);
|
||||
|
||||
.display-graph .graph-placeholder {
|
||||
width: 750px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.display-table {
|
||||
.slickgrid {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grade-distributions-widget {
|
||||
margin-bottom: ($baseline * 2);
|
||||
|
||||
.last-updated {
|
||||
line-height: 2.2em;
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.display-graph .graph-placeholder {
|
||||
width: 750px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.display-text {
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
input[name="subject"] {
|
||||
width:600px;
|
||||
}
|
||||
|
||||
@@ -159,9 +159,6 @@ function goto( mode)
|
||||
%if show_email_tab:
|
||||
| <a href="#" onclick="goto('Email')" class="${modeflag.get('Email')}">${_("Email")}</a>
|
||||
%endif
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'):
|
||||
| <a href="#" onclick="goto('Analytics');" class="${modeflag.get('Analytics')}">${_("Analytics")}</a>
|
||||
%endif
|
||||
%if settings.FEATURES.get('CLASS_DASHBOARD'):
|
||||
| <a href="#" onclick="goto('Metrics');" class="${modeflag.get('Metrics')}">${_("Metrics")}</a>
|
||||
%endif
|
||||
@@ -402,203 +399,6 @@ function goto( mode)
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if modeflag.get('Analytics'):
|
||||
|
||||
%if not any(analytics_results.values()):
|
||||
<p>${_("No Analytics are available at this time.")}</p>
|
||||
%endif
|
||||
|
||||
%if analytics_results.get("StudentsDropoffPerDay"):
|
||||
<p>
|
||||
${_("Student activity day by day")}
|
||||
(${analytics_results["StudentsDropoffPerDay"]['time']})
|
||||
</p>
|
||||
<div>
|
||||
<table class="stat_table">
|
||||
<tr>
|
||||
<th>${_("Day")}</th>
|
||||
<th>${_("Students")}</th>
|
||||
</tr>
|
||||
%for row in analytics_results['StudentsDropoffPerDay']['data']:
|
||||
<tr>
|
||||
## For now, just discard the non-date portion
|
||||
<td>${row['last_day'].split("T")[0]}</td>
|
||||
<td>${row['num_students']}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
%endif
|
||||
<br/>
|
||||
%if analytics_results.get("ProblemGradeDistribution"):
|
||||
<p>
|
||||
${_("Score distribution for problems")}
|
||||
(${analytics_results["ProblemGradeDistribution"]['time']})
|
||||
</p>
|
||||
<div>
|
||||
<table class="stat_table">
|
||||
<tr>
|
||||
<th>${_("Problem")}</th>
|
||||
<th>${_("Max")}</th>
|
||||
<th colspan="99">${_("Points Earned (Num Students)")}</th>
|
||||
</tr>
|
||||
%for row in analytics_results['ProblemGradeDistribution']['data']:
|
||||
<tr>
|
||||
<td>${row['block_id']}</td>
|
||||
<td>${max(grade_record['max_grade'] for grade_record in row["grade_info"])}
|
||||
%for grade_record in row["grade_info"]:
|
||||
<td>
|
||||
%if isinstance(grade_record["grade"], float):
|
||||
${"{grade:.2f}".format(**grade_record)}
|
||||
%else:
|
||||
${"{grade}".format(**grade_record)}
|
||||
%endif
|
||||
(${grade_record["num_students"]})
|
||||
</td>
|
||||
%endfor
|
||||
</tr>
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%if modeflag.get('Metrics'):
|
||||
%if not any (metrics_results.values()):
|
||||
<p>${_("There is no data available to display at this time.")}</p>
|
||||
%else:
|
||||
<%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/>
|
||||
<%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/>
|
||||
|
||||
<script>
|
||||
${d3_stacked_bar_graph.body()}
|
||||
</script>
|
||||
|
||||
<div id="metrics"></div>
|
||||
|
||||
<h3 class="attention">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3>
|
||||
|
||||
%for i in range(0,len(metrics_results['section_display_name'])):
|
||||
<div class="metrics-container" id="metrics_section_${i}">
|
||||
<h2>${_("Section:")} ${metrics_results['section_display_name'][i]}</h2>
|
||||
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
|
||||
<div class="metrics-left" id="metric_opened_${i}">
|
||||
<h3>${_("Count of Students that Opened a Subsection")}</h3>
|
||||
<p class="loading"><i class="icon fa fa-spinner fa-spin fa-large"></i>${_("Loading")}</p>
|
||||
</div>
|
||||
<div class="metrics-right" id="metric_grade_${i}">
|
||||
<h3>${_("Grade Distribution per Problem")}</h3>
|
||||
%if not metrics_results['section_has_problem'][i]:
|
||||
<p>${_("There are no problems in this section.")}</p>
|
||||
%else:
|
||||
<p class="loading"><i class="icon fa fa-spinner fa-spin fa-large"></i>${_("Loading")}</p>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
<script>
|
||||
var allSubsectionTooltipArr = new Array();
|
||||
var allProblemTooltipArr = new Array();
|
||||
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
|
||||
</script>
|
||||
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%if modeflag.get('Analytics In Progress'):
|
||||
|
||||
##This is not as helpful as it could be -- let's give full point distribution
|
||||
##instead.
|
||||
%if analytics_results.get("StudentsPerProblemCorrect"):
|
||||
<p>
|
||||
${_("Students answering correctly")}
|
||||
(${analytics_results["StudentsPerProblemCorrect"]['time']})
|
||||
</p>
|
||||
<div class="divScroll">
|
||||
<table class="stat_table">
|
||||
<tr>
|
||||
<th>${_("Problem")}</th>
|
||||
<th>${_("Number of students")}</th>
|
||||
</tr>
|
||||
%for row in analytics_results['StudentsPerProblemCorrect']['data']:
|
||||
<tr>
|
||||
<td>${row['module_id'].split('/')[-1]}</td>
|
||||
<td>${row['count']}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<p>
|
||||
${_("Student distribution per country, all courses, Sep 12 to Oct 17, 1 server (shown here as an example):")}
|
||||
</p>
|
||||
|
||||
<div id="posts-list" class="clearfix">
|
||||
<figure>
|
||||
<div id="world-map-students" style="width: 720px; height: 400px"></div>
|
||||
<script>
|
||||
var student_data = {BD : '300', BE : '156', BF : '7', BG : '246', BA : '62', BB : '1', BN : '7', BO : '61', JP : '153', BI : '4', BJ : '6', BT : '11', JM : '32', JO : '67', WS : '1', BR : '1941', BS : '5', JE : '6', BY : '166', BZ : '4', RU : '1907', RW : '50', RS : '128', TL : '1', RE : '2', A2 : '59', TJ : '9', RO : '232', GU : '3', GT : '76', GR : '565', BH : '22', GY : '6', GG : '2', GF : '1', GE : '22', GD : '7', GB : '2023', GA : '4', GM : '18', GL : '2', GI : '1', GH : '393', OM : '25', TN : '143', BW : '26', HR : '76', HT : '38', HU : '259', HK : '103', HN : '51', AD : '1', PR : '40', PS : '38', PT : '487', PY : '38', PA : '21', PG : '11', PE : '342', PK : '1833', PH : '571', TM : '1', PL : '736', ZM : '61', EE : '67', EG : '961', ZA : '184', EC : '118', AL : '44', AO : '10', SB : '2', EU : '183', ET : '153', SO : '1', ZW : '42', KY : '3', ES : '1954', ER : '3', ME : '6', MD : '26', MG : '10', UY : '64', UZ : '40', MM : '21', ML : '4', MO : '3', MN : '49', US : '11899', MU : '11', MT : '15', MW : '41', MV : '5', MP : '4', MR : '1', IM : '2', UG : '133', MY : '207', MX : '844', AT : '83', FR : '446', MA : '175', A1 : '167', AX : '1', FI : '97', FJ : '9', NI : '23', NL : '240', NO : '110', NA : '27', NC : '1', NE : '4', NG : '753', NZ : '98', NP : '200', CI : '9', CH : '144', CO : '851', CN : '282', CM : '82', CL : '243', CA : '1129', CD : '7', CZ : '161', CY : '26', CR : '137', CV : '11', CU : '15', SZ : '6', SY : '58', KG : '47', KE : '282', SR : '5', KI : '1', KH : '40', SV : '155', KM : '1', ST : '1', SK : '66', KR : '141', SI : '70', KP : '1', KW : '28', SN : '16', SL : '11', KZ : '174', SA : '352', SG : '217', SE : '172', SD : '61', DO : '104', DM : '5', DJ : '6', DK : '105', DE : '941', YE : '90', DZ : '281', MK : '28', TZ : '124', LC : '5', LA : '7', TW : '115', TT : '33', TR : '288', LK : '96', LV : '52', TO : '2', LT : '114', LU : '21', LR : '9', LS : '9', TH : '84', TG : '11', LY : '15', VC : '6', AE : '151', VE : '180', AG : '1', AF : '21', IQ : '29', VI : '1', IS : '14', IR : '153', AM : '37', IT : '365', VN : '269', AP : '23', AR : '258', AU : '661', IL : '159', AW : '3', IN : '7836', LB : '28', AZ : '22', IE : '210', ID : '382', UA : '860', QA : '23', MZ : '8'};
|
||||
$(function(){
|
||||
$('#world-map-students').vectorMap({
|
||||
map: 'world_mill_en',
|
||||
backgroundColor: '#eeeeee',
|
||||
series: {
|
||||
regions: [{
|
||||
values: student_data,
|
||||
scale: ['#C8EEFF', '#0071A4'],
|
||||
normalizeFunction: 'polynomial'
|
||||
}]
|
||||
},
|
||||
onRegionLabelShow: function(event, label, code){
|
||||
label.text(label.text() + ': ' + (student_data[code] != null ? student_data[code] : 0));
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
|
||||
## <p>Number of students who dropped off per day before becoming inactive:</p>
|
||||
##
|
||||
## % if dropoff_per_day is not None:
|
||||
## % if dropoff_per_day['status'] == 'success':
|
||||
## <div class="divScroll">
|
||||
## <table class="stat_table">
|
||||
## <tr><th>Day</th><th>Number of students</th></tr>
|
||||
## % for k,v in dropoff_per_day['data'].items():
|
||||
## <tr> <td>${k}</td> <td>${v}</td> </tr>
|
||||
## % endfor
|
||||
## </table>
|
||||
## </div>
|
||||
## % else:
|
||||
## <i> ${dropoff_per_day['error']}</i>
|
||||
## % endif
|
||||
## % else:
|
||||
## <i> null data </i>
|
||||
## % endif
|
||||
## </p>
|
||||
##
|
||||
|
||||
|
||||
## <p>
|
||||
## <h2>Daily activity (online version):</h2>
|
||||
## <table class="stat_table">
|
||||
## <tr><th>Day</td><th>Number of students</td></tr>
|
||||
## % for k,v in daily_activity_json['data'].items():
|
||||
## <tr>
|
||||
## <td>${k}</td> <td>${v}</td>
|
||||
## </tr>
|
||||
## % endfor
|
||||
## </table>
|
||||
## </p>
|
||||
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if datatable and modeflag.get('Psychometrics') is None:
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -1,71 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%page args="section_data"/>
|
||||
|
||||
<script type="text/template" id="profile-distribution-widget-template">
|
||||
<div class="profile-distribution-widget">
|
||||
<div class="header">
|
||||
<h2 class="title"> {{title}} </h2>
|
||||
</div>
|
||||
<div class="view">
|
||||
<div class="display-errors"></div>
|
||||
<div class="display-text"></div>
|
||||
<div class="display-graph"></div>
|
||||
<div class="display-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
%if settings.FEATURES['ENABLE_INSTRUCTOR_ANALYTICS']:
|
||||
<script type="text/template" id="grade-distributions-widget-template">
|
||||
<div class="grade-distributions-widget">
|
||||
<div class="header">
|
||||
<h2 class="title"> ${_("Score Distribution")} </h2>
|
||||
<p>${_("The chart below displays the score distribution for each standard problem in your class, specified by the problem's url name.")}
|
||||
${_("Scores are shown without weighting applied, so if your problem contains 2 questions, it will display as having a total of 2 points.")}</p>
|
||||
<br />
|
||||
${_("Problem")}: <select class="problem-selector">
|
||||
<option> ${_("Loading problem list...")} </option>
|
||||
</select>
|
||||
<div class="last-updated"></div>
|
||||
</div>
|
||||
<div class="view">
|
||||
<div class="display-errors"></div>
|
||||
<div class="display-text"></div>
|
||||
<div class="display-graph"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<div class="grade-distributions-widget-container"
|
||||
data-endpoint="${ section_data['proxy_legacy_analytics_url'] }"
|
||||
>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
%endif
|
||||
|
||||
%if settings.FEATURES['DISPLAY_ANALYTICS_DEMOGRAPHICS']:
|
||||
<div class="profile-distribution-widget-container"
|
||||
data-title="${_("Year of Birth")}"
|
||||
data-feature="year_of_birth"
|
||||
data-endpoint="${ section_data['get_distribution_url'] }"
|
||||
></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="profile-distribution-widget-container"
|
||||
data-title="${_("Gender Distribution")}"
|
||||
data-feature="gender"
|
||||
data-endpoint="${ section_data['get_distribution_url'] }"
|
||||
></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="profile-distribution-widget-container"
|
||||
data-title="${_("Level of Education")}"
|
||||
data-feature="level_of_education"
|
||||
data-endpoint="${ section_data['get_distribution_url'] }"
|
||||
></div>
|
||||
%elif section_data['demographic_message']:
|
||||
<p><em>${section_data['demographic_message']}</em></p>
|
||||
%endif
|
||||
<div>
|
||||
<p><em>${section_data['insights_message']}</em></p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user