Commit 59df7008 by Kristin Stephens Committed by Giulio Gratta

Latest metrics tab

Merge of latest metrics tab code into edx-west/release
Note that the metrics tab is in the legacy dash only
for now.
parent 175f59b4
......@@ -4,7 +4,6 @@ Computes the data to display on the Instructor Dashboard
from courseware import models
from django.db.models import Count
from queryable_student_module.models import StudentModuleExpand, Log
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
......@@ -47,38 +46,6 @@ def get_problem_grade_distribution(course_id):
return prob_grade_distrib
def get_problem_attempt_distrib(course_id, max_attempts=10):
"""
Returns the attempt distribution per problem for the course.
`course_id` the course ID for the course interested in
`max_attempts` any students with more attempts than this are grouped together (default 10)
Output is a dicts, where the key is the problem `module_id` and the value is an array where the first index is
the number of students that only attempted once, second is two times, etc. The last index is all students that
attempted more than `max_attempts` times.
"""
db_query = StudentModuleExpand.objects.filter(
course_id__exact=course_id,
attempts__isnull=False,
module_type__exact="problem",
).values('module_state_key', 'attempts').annotate(count_attempts=Count('attempts'))
prob_attempts_distrib = {}
for row in db_query:
curr_problem = row['module_state_key']
if curr_problem not in prob_attempts_distrib:
prob_attempts_distrib[curr_problem] = [0] * (max_attempts + 1)
if row['attempts'] > max_attempts:
prob_attempts_distrib[curr_problem][max_attempts] += row['count_attempts']
else:
prob_attempts_distrib[curr_problem][row['attempts'] - 1] = row['count_attempts']
return prob_attempts_distrib
def get_sequential_open_distrib(course_id):
"""
Returns the number of students that opened each subsection/sequential of the course
......@@ -100,25 +67,6 @@ def get_sequential_open_distrib(course_id):
return sequential_open_distrib
def get_last_populate(course_id, script_id):
"""
Returns the timestamp when a script was last run for a course.
`course_id` the course ID for the course interested in
`script_id` string identifying the populate script interested in
Returns None if there is no known time the script was last run for that course.
"""
db_query = Log.objects.filter(course_id__exact=course_id, script_id__exact=script_id)
if len(db_query) > 0:
return db_query[0].created # Model is sorted last first
else:
return None
def get_problem_set_grade_distribution(course_id, problem_set):
"""
Returns the grade distribution for the problems specified in `problem_set`.
......@@ -226,69 +174,6 @@ def get_d3_problem_grade_distribution(course_id):
return d3_data
def get_d3_problem_attempt_distribution(course_id, max_attempts=10):
"""
Returns problem attempt distribution information for each section, data already in format for d3 function.
`course_id` the course ID for the course interested in
`max_attempts` any students with more attempts than this are grouped together (default: 10)
Returns an array of dicts in the order of the sections. Each dict has:
'display_name' - display name for the section
'data' - data for the attempt distribution of problems in this section for d3_stacked_bar_graph
"""
prob_attempts_distrib = get_problem_attempt_distrib(course_id, max_attempts)
d3_data = []
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
for section in course.get_children():
curr_section = {}
curr_section['display_name'] = own_metadata(section)['display_name']
data = []
c_subsection = 0
for subsection in section.get_children():
c_subsection += 1
c_unit = 0
for unit in subsection.get_children():
c_unit += 1
c_problem = 0
for child in unit.get_children():
if (child.location.category == 'problem'):
c_problem += 1
stack_data = []
label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem)
if child.location.url() in prob_attempts_distrib:
attempts_distrib = prob_attempts_distrib[child.location.url()]
problem_name = own_metadata(child)['display_name']
for i in range(0, max_attempts + 1):
color = (i + 1 if i != max_attempts else "{0}+".format(max_attempts))
tooltip = "{0} {3} - {1} Student(s) had {2} attempt(s)".format(
label, attempts_distrib[i], color, problem_name
)
stack_data.append({
'color': color,
'value': attempts_distrib[i],
'tooltip': tooltip,
})
problem = {
'xValue': label,
'stackData': stack_data,
}
data.append(problem)
curr_section['data'] = data
d3_data.append(curr_section)
return d3_data
def get_d3_sequential_open_distribution(course_id):
"""
Returns how many students opened a sequential/subsection for each section, data already in format for d3 function.
......
from mock import Mock, patch
import json
from django.test.utils import override_settings
from django.test import TestCase
from django.core import management
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from courseware.tests.factories import StudentModuleFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
from courseware.models import StudentModule
from capa.tests.response_xml_factory import StringResponseXMLFactory
from xmodule.modulestore import Location
from queryable_student_module.management.commands import populate_studentmoduleexpand
from xmodule.course_module import CourseDescriptor
from class_dashboard.dashboard_data import get_problem_grade_distribution, get_problem_attempt_distrib, get_sequential_open_distrib, \
get_last_populate, get_problem_set_grade_distribution, get_d3_problem_grade_distribution, \
get_d3_problem_attempt_distribution, get_d3_sequential_open_distribution, \
get_d3_section_grade_distribution, get_section_display_name, get_array_section_has_problem
from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib,
get_problem_set_grade_distribution, get_d3_problem_grade_distribution,
get_d3_sequential_open_distribution, get_d3_section_grade_distribution,
get_section_display_name, get_array_section_has_problem
)
from class_dashboard.views import has_instructor_access_for_class
USER_COUNT = 11
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestGetProblemGradeDistribution(ModuleStoreTestCase):
"""
......@@ -33,15 +33,14 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
- simple test, make sure output correct
- test when a problem has two max_grade's, should just take the larger value
"""
def setUp(self):
self.command = 'populate_studentmoduleexpand'
self.script_id = "studentmoduleexpand"
self.instructor = AdminFactory.create()
self.client.login(username=self.instructor.username, password='test')
self.attempts = 3
self.course = CourseFactory.create()
section = ItemFactory.create(
parent_location=self.course.location,
category="chapter",
......@@ -50,20 +49,19 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
sub_section = ItemFactory.create(
parent_location=section.location,
category="sequential",
# metadata={'graded': True, 'format': 'Homework'}
)
unit = ItemFactory.create(
parent_location=sub_section.location,
category="vertical",
metadata={'graded': True, 'format': 'Homework'}
)
self.users = [UserFactory.create() for _ in xrange(USER_COUNT)]
for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT - 1):
category = "problem"
item = ItemFactory.create(
......@@ -72,11 +70,11 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
for j, user in enumerate(self.users):
StudentModuleFactory.create(
grade=1 if i < j else 0,
max_grade=1 if i< j else 0.5,
max_grade=1 if i < j else 0.5,
student=user,
course_id=self.course.id,
module_state_key=Location(item.location).url(),
......@@ -98,36 +96,13 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
max_grade = prob_grade_distrib[problem]['max_grade']
self.assertEquals(1, max_grade)
def test_get_problem_attempt_distribution(self):
# Call command
management.call_command(self.command, self.course.id)
prob_attempts_distrib = get_problem_attempt_distrib(self.course.id)
for problem in prob_attempts_distrib:
num_attempts = prob_attempts_distrib[problem][self.attempts -1]
self.assertEquals(USER_COUNT, num_attempts)
def test_get_sequential_open_distibution(self):
sequential_open_distrib = get_sequential_open_distrib(self.course.id)
for problem in sequential_open_distrib:
num_students = sequential_open_distrib[problem]
self.assertEquals(USER_COUNT, num_students)
def test_get_last_populate(self):
timestamp = get_last_populate(self.course.id, self.script_id)
self.assertEquals(timestamp, None)
management.call_command(self.command, self.course.id)
timestamp = get_last_populate(self.course.id, self.script_id)
self.assertNotEquals(timestamp, None)
def test_get_problemset_grade_distrib(self):
......@@ -144,10 +119,8 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
sum_attempts += item[1]
self.assertEquals(USER_COUNT, sum_attempts)
def test_get_d3_problem_grade_distrib(self):
# @patch('class_dashboard.dashboard_data.get_problem_grade_distribution')
def test_get_d3_problem_grade_distrib(self): #, mock_get_data):
d3_data = get_d3_problem_grade_distribution(self.course.id)
for data in d3_data:
for stack_data in data['data']:
......@@ -156,54 +129,51 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values)
def test_get_d3_problem_attempt_distrib(self):
# Call command
management.call_command(self.command, self.course.id)
d3_data = get_d3_problem_attempt_distribution(self.course.id)
for data in d3_data:
for stack_data in data['data']:
sum_values = 0
for problem in stack_data['stackData']:
sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values)
def test_get_d3_sequential_open_distrib(self):
d3_data = get_d3_sequential_open_distribution(self.course.id)
for data in d3_data:
for stack_data in data['data']:
for problem in stack_data['stackData']:
value = problem['value']
self.assertEquals(0, value)
self.assertEquals(0, value)
def test_get_d3_section_grade_distrib(self):
d3_data = get_d3_section_grade_distribution(self.course.id, 0)
for stack_data in d3_data:
sum_values = 0
for problem in stack_data['stackData']:
sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values)
def test_get_section_display_name(self):
section_display_name = get_section_display_name(self.course.id)
self.assertMultiLineEqual(section_display_name[0], 'test factory section')
def test_get_array_section_has_problem(self):
b_section_has_problem = get_array_section_has_problem(self.course.id)
print b_section_has_problem
self.assertEquals(b_section_has_problem[0], True)
def test_dashboard(self):
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(
url,
{
'idash_mode': 'Metrics'
}
)
self.assertContains(response, '<h2>Course Statistics At A Glance</h2>')
def test_has_instructor_access_for_class(self): #user, course_id):
"""
Test for instructor access
"""
ret_val = has_instructor_access_for_class(self.instructor, self.course.id)
self.assertEquals(ret_val, True)
......@@ -5,19 +5,21 @@ from django.test.client import RequestFactory
from django.utils import simplejson
from class_dashboard import views
from student.tests.factories import AdminFactory
class TestViews(TestCase):
def setUp(self):
self.request_factory = RequestFactory()
self.request = self.request_factory.get('')
self.request.user = None
self.simple_data = {'test': 'test'}
@patch('class_dashboard.dashboard_data.get_d3_problem_attempt_distribution')
@patch('class_dashboard.dashboard_data.get_d3_problem_grade_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_problem_attempt_distribution_has_access(self, has_access, data_method):
def test_all_problem_grade_distribution_has_access(self, has_access, data_method):
"""
Test returns proper value when have proper access
"""
......@@ -25,23 +27,20 @@ class TestViews(TestCase):
data_method.return_value = self.simple_data
response = views.all_problem_attempt_distribution(self.request, 'test/test/test')
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.dashboard_data.get_d3_problem_grade_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_problem_grade_distribution_has_access(self, has_access, data_method):
def test_all_problem_grade_distribution_no_access(self, has_access, data_method):
"""
Test returns proper value when have proper access
Test for no access
"""
has_access.return_value = True
data_method.return_value = self.simple_data
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
has_access.return_value = False
response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1')
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
@patch('class_dashboard.dashboard_data.get_d3_sequential_open_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class')
......@@ -57,6 +56,17 @@ class TestViews(TestCase):
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.dashboard_data.get_d3_sequential_open_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_all_sequential_open_distribution_no_access(self, has_access, data_method):
"""
Test for no access
"""
has_access.return_value = False
response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1')
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
@patch('class_dashboard.dashboard_data.get_d3_section_grade_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_section_problem_grade_distribution_has_access(self, has_access, data_method):
......@@ -70,3 +80,14 @@ class TestViews(TestCase):
response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1')
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.dashboard_data.get_d3_section_grade_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class')
def test_section_problem_grade_distribution_no_access(self, has_access, data_method):
"""
Test for no access
"""
has_access.return_value = False
response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1')
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
......@@ -16,28 +16,32 @@ def has_instructor_access_for_class(user, course_id):
"""
course = get_course_with_access(user, course_id, 'staff', depth=None)
return has_access(user, course, 'instructor')
def all_problem_attempt_distribution(request, course_id):
"""
Creates a json with the attempt distribution for all the problems in the course.
`request` django request
`course_id` the course ID for the course interested in
Returns the format in dashboard_data.get_d3_problem_attempt_distribution
"""
json = {}
# Only instructor for this particular course can request this information
if has_instructor_access_for_class(request.user, course_id):
json = dashboard_data.get_d3_problem_attempt_distribution(course_id)
else:
json = {'error': "Access Denied: User does not have access to this course's data"}
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
#ToDo returning false hangs page.
return has_access(user, course, 'staff')
# def all_problem_attempt_distribution(request, course_id):
# """
# Creates a json with the attempt distribution for all the problems in the course.
#
# `request` django request
#
# `course_id` the course ID for the course interested in
#
# Returns the format in dashboard_data.get_d3_problem_attempt_distribution
# """
# json = {}
#
# # Only instructor for this particular course can request this information
# if has_instructor_access_for_class(request.user, course_id):
# json = dashboard_data.get_d3_problem_attempt_distribution(course_id)
# else:
# json = {'error': "Access Denied: User does not have access to this course's data"}
#
# return HttpResponse(simplejson.dumps(json), mimetype="application/json")
def all_sequential_open_distribution(request, course_id):
......
......@@ -25,6 +25,8 @@ from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment
from bulk_email.models import CourseAuthorization
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
......@@ -50,8 +52,9 @@ def instructor_dashboard_2(request, course_id):
_section_course_info(course_id, access),
_section_membership(course_id, access),
_section_student_admin(course_id, access),
_section_data_download(course_id),
_section_analytics(course_id),
_section_data_download(course_id, access),
_section_analytics(course_id, access),
# _section_metrics(course_id, access),
]
# Gate access to course email by feature flag & by course-specific authorization
......@@ -159,14 +162,16 @@ def _section_student_admin(course_id, access):
return section_data
def _section_data_download(course_id):
def _section_data_download(course_id, access):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'data_download',
'section_display_name': _('Data Download'),
'access': access,
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}),
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}),
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_id}),
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
}
return section_data
......@@ -187,12 +192,25 @@ def _section_send_email(course_id, access, course):
return section_data
def _section_analytics(course_id):
def _section_analytics(course_id, access):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'analytics',
'section_display_name': _('Analytics'),
'access': access,
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}),
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
}
return section_data
def _section_metrics(course_id, access):
"""Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'metrics',
'section_display_name': ('Metrics'),
'access': access,
'sub_section_display_name': get_section_display_name(course_id),
'section_has_problem': get_array_section_has_problem(course_id)
}
return section_data
......@@ -821,8 +821,9 @@ def instructor_dashboard(request, course_id):
# Metrics
metrics_results = {}
if settings.MITX_FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics':
metrics_results['attempts_timestamp'] = dashboard_data.get_last_populate(course_id, "studentmoduleexpand")
# if settings.MITX_FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics':
if idash_mode == 'Metrics':
# metrics_results['attempts_timestamp'] = dashboard_data.get_last_populate(course_id, "studentmoduleexpand")
metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_id)
metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_id)
......@@ -874,35 +875,35 @@ def instructor_dashboard(request, course_id):
#----------------------------------------
# context for rendering
context = {
'course': course,
'staff_access': True,
'admin_access': request.user.is_staff,
'instructor_access': instructor_access,
'forum_admin_access': forum_admin_access,
'datatable': datatable,
'course_stats': course_stats,
'msg': msg,
'modeflag': {idash_mode: 'selectedmode'},
'studio_url': studio_url,
'to_option': email_to_option, # email
'subject': email_subject, # email
'editor': email_editor, # email
'email_msg': email_msg, # email
'show_email_tab': show_email_tab, # email
'problems': problems, # psychometrics
'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location),
'instructor_tasks': instructor_tasks,
'offline_grade_log': offline_grades_available(course_id),
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'analytics_results': analytics_results,
'disable_buttons': disable_buttons,
'metrics_results': metrics_results,
}
context = {'course': course,
'staff_access': True,
'admin_access': request.user.is_staff,
'instructor_access': instructor_access,
'forum_admin_access': forum_admin_access,
'datatable': datatable,
'course_stats': course_stats,
'msg': msg,
'modeflag': {idash_mode: 'selectedmode'},
'studio_url': studio_url,
'to_option': email_to_option, # email
'subject': email_subject, # email
'editor': email_editor, # email
'email_msg': email_msg, # email
'show_email_tab': show_email_tab, # email
'problems': problems, # psychometrics
'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location),
'instructor_tasks': instructor_tasks,
'offline_grade_log': offline_grades_available(course_id),
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'analytics_results': analytics_results,
'disable_buttons': disable_buttons,
'metrics_results': metrics_results,
}
if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id})
......
......@@ -1064,9 +1064,6 @@ VERIFY_STUDENT = {
"DAYS_GOOD_FOR" : 365, # How many days is a verficiation good for?
}
########################## QUERYABLE TABLES ########################
INSTALLED_APPS += ('queryable_student_module',)
########################## CLASS DASHBOARD ########################
INSTALLED_APPS += ('class_dashboard',)
MITX_FEATURES['CLASS_DASHBOARD'] = False
......
......@@ -167,6 +167,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
,
constructor: window.InstructorDashboard.sections.Analytics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
,
# constructor: window.InstructorDashboard.sections.Metrics
# $element: idash_content.find ".#{CSS_IDASH_SECTION}#metrics"
]
sections_to_initialize.map ({constructor, $element}) ->
......
# METRICS 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
#Metrics Section
class Metrics
constructor: (@$section) ->
@$section.data 'wrapper', @
# handler for when the section title is clicked.
onClickTitle: ->
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
if _?
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
Metrics: Metrics
......@@ -29,6 +29,8 @@ $(function () {
barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
barGraphOpened.drawGraph();
$('svg').siblings('.loading').remove();
}
i+=1;
......@@ -57,36 +59,8 @@ $(function () {
barGraphGrade.legend.width += 2;
barGraphGrade.drawGraph();
}
i+=1;
}
});
d3.json("${reverse('all_problem_attempt_distribution', kwargs=dict(course_id=course_id))}", function(error, json) {
var section, paramAttempt, barGraphAttempt;
var i, curr_id;
i = 0;
for (section in json) {
curr_id = "#${id_attempt_prefix}"+i;
paramAttempt = {
data: json[section].data,
width: $(curr_id).width(),
height: $(curr_id).height()-25, // Account for header
tag: "attempt"+i,
bVerticalXAxisLabel : true,
};
if ( paramAttempt.data.length > 0 ) {
barGraphAttempt = edx_d3CreateStackedBarGraph(paramAttempt, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i));
barGraphAttempt.scale.stackColor
.range(["#c3c4cd","#b0b4d1","#9ca3d6","#8993da","#7682de","#6372e3",
"#4f61e7","#3c50eb","#2940ef","#1530f4","#021ff8"]);
barGraphAttempt.legend.width += 2;
barGraphAttempt.drawGraph();
$('svg').siblings('.loading').remove();
}
i+=1;
......
......@@ -105,6 +105,7 @@ textarea {
width: 100%;
float: left;
clear: both;
margin-top: 25px;
}
.metrics-left {
position: relative;
......@@ -132,6 +133,22 @@ textarea {
fill: white;
}
p.loading {
padding-top: 100px;
text-align: center;
}
p.nothing {
padding-top: 25px;
}
h3.attention {
padding: 10px;
border: 1px solid #999;
border-radius: 5px;
margin-top: 25px;
}
</style>
<script language="JavaScript" type="text/javascript">
......@@ -697,6 +714,8 @@ function goto( mode)
</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}">
......@@ -704,17 +723,14 @@ function goto( mode)
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
<div class="metrics-left" id="metric_opened_${i}">
<h3>Count of Students Opened a Subsection</h3>
<p class="loading"><i class="icon-spinner icon-spin icon-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>
%endif
</div>
<div class="metrics-right" id="metric_attempts_${i}">
<h3>Attempt Distribution per Problem (Last update: ${metrics_results['attempts_timestamp']})</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-spinner icon-spin icon-large"></i> Loading...</p>
%endif
</div>
</div>
......
<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
<style type="text/css">
.metrics-container {
position: relative;
width: 100%;
float: left;
clear: both;
margin-top: 25px;
}
.metrics-left {
position: relative;
width: 30%;
height: 640px;
float: left;
margin-right: 2.5%;
}
.metrics-left svg {
width: 100%;
}
.metrics-right {
position: relative;
width: 65%;
height: 295px;
float: left;
margin-left: 2.5%;
margin-bottom: 25px;
}
.metrics-right svg {
width: 100%;
}
.metrics-tooltip {
width: 250px;
background-color: lightgray;
padding: 3px;
}
.stacked-bar-graph-legend {
fill: white;
}
p.loading {
padding-top: 100px;
text-align: center;
}
p.nothing {
padding-top: 25px;
}
h3.attention {
padding: 10px;
border: 1px solid #999;
border-radius: 5px;
margin-top: 25px;
}
</style>
<script>
${d3_stacked_bar_graph.body()}
</script>
%if not any (section_data.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"/>
<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(section_data['sub_section_display_name'])):
<div class="metrics-container" id="metrics_section_${i}">
<h2>Section: ${section_data['sub_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 Opened a Subsection</h3>
<p class="loading"><i class="icon-spinner icon-spin icon-large"></i> Loading...</p>
</div>
<div class="metrics-right" id="metric_grade_${i}">
<h3>Grade Distribution per Problem</h3>
%if not section_data['section_has_problem'][i]:
<p class="nothing">${_("There are no problems in this section.")}</p>
%else:
<p class="loading"><i class="icon-spinner icon-spin icon-large"></i> Loading...</p>
%endif
</div>
</div>
%endfor
<script>
$(function () {
var firstLoad = true;
$('.instructor-nav a').click(function () {
if ($(this).data('section') === "metrics" && firstLoad) {
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)}
firstLoad = false;
}
});
if (window.location.hash === "#view-metrics") {
$('.instructor-nav a[data-section="metrics"]').click();
}
});
</script>
%endif
......@@ -371,8 +371,6 @@ if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR
if settings.MITX_FEATURES.get('CLASS_DASHBOARD'):
urlpatterns += (
# Json request data for metrics for entire course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_attempt_distribution$',
'class_dashboard.views.all_problem_attempt_distribution', name="all_problem_attempt_distribution"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distribution$',
'class_dashboard.views.all_sequential_open_distribution', name="all_sequential_open_distribution"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
......@@ -383,7 +381,6 @@ if settings.MITX_FEATURES.get('CLASS_DASHBOARD'):
'class_dashboard.views.section_problem_grade_distribution', name="section_problem_grade_distribution"),
)
if settings.ENABLE_JASMINE:
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
......
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