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 ...@@ -4,7 +4,6 @@ Computes the data to display on the Instructor Dashboard
from courseware import models from courseware import models
from django.db.models import Count from django.db.models import Count
from queryable_student_module.models import StudentModuleExpand, Log
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -47,38 +46,6 @@ def get_problem_grade_distribution(course_id): ...@@ -47,38 +46,6 @@ def get_problem_grade_distribution(course_id):
return prob_grade_distrib 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): def get_sequential_open_distrib(course_id):
""" """
Returns the number of students that opened each subsection/sequential of the course Returns the number of students that opened each subsection/sequential of the course
...@@ -100,25 +67,6 @@ def get_sequential_open_distrib(course_id): ...@@ -100,25 +67,6 @@ def get_sequential_open_distrib(course_id):
return sequential_open_distrib 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): def get_problem_set_grade_distribution(course_id, problem_set):
""" """
Returns the grade distribution for the problems specified in `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): ...@@ -226,69 +174,6 @@ def get_d3_problem_grade_distribution(course_id):
return d3_data 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): 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. Returns how many students opened a sequential/subsection for each section, data already in format for d3 function.
......
from mock import Mock, patch from mock import Mock, patch
import json import json
from django.test.utils import override_settings from django.test.utils import override_settings
from django.test import TestCase from django.test import TestCase
from django.core import management from django.core import management
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from courseware.tests.factories import StudentModuleFactory 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 courseware.models import StudentModule
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
from xmodule.modulestore import Location from xmodule.modulestore import Location
from queryable_student_module.management.commands import populate_studentmoduleexpand
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib,
from class_dashboard.dashboard_data import get_problem_grade_distribution, get_problem_attempt_distrib, get_sequential_open_distrib, \ get_problem_set_grade_distribution, get_d3_problem_grade_distribution,
get_last_populate, get_problem_set_grade_distribution, get_d3_problem_grade_distribution, \ get_d3_sequential_open_distribution, get_d3_section_grade_distribution,
get_d3_problem_attempt_distribution, get_d3_sequential_open_distribution, \ get_section_display_name, get_array_section_has_problem
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 USER_COUNT = 11
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestGetProblemGradeDistribution(ModuleStoreTestCase): class TestGetProblemGradeDistribution(ModuleStoreTestCase):
""" """
...@@ -33,15 +33,14 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -33,15 +33,14 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
- simple test, make sure output correct - simple test, make sure output correct
- test when a problem has two max_grade's, should just take the larger value - test when a problem has two max_grade's, should just take the larger value
""" """
def setUp(self): def setUp(self):
self.command = 'populate_studentmoduleexpand' self.instructor = AdminFactory.create()
self.script_id = "studentmoduleexpand" self.client.login(username=self.instructor.username, password='test')
self.attempts = 3 self.attempts = 3
self.course = CourseFactory.create() self.course = CourseFactory.create()
section = ItemFactory.create( section = ItemFactory.create(
parent_location=self.course.location, parent_location=self.course.location,
category="chapter", category="chapter",
...@@ -50,20 +49,19 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -50,20 +49,19 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
sub_section = ItemFactory.create( sub_section = ItemFactory.create(
parent_location=section.location, parent_location=section.location,
category="sequential", category="sequential",
# metadata={'graded': True, 'format': 'Homework'}
) )
unit = ItemFactory.create( unit = ItemFactory.create(
parent_location=sub_section.location, parent_location=sub_section.location,
category="vertical", category="vertical",
metadata={'graded': True, 'format': 'Homework'} metadata={'graded': True, 'format': 'Homework'}
) )
self.users = [UserFactory.create() for _ in xrange(USER_COUNT)] self.users = [UserFactory.create() for _ in xrange(USER_COUNT)]
for user in self.users: for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id) CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT - 1): for i in xrange(USER_COUNT - 1):
category = "problem" category = "problem"
item = ItemFactory.create( item = ItemFactory.create(
...@@ -72,11 +70,11 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -72,11 +70,11 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
data=StringResponseXMLFactory().build_xml(answer='foo'), data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'} metadata={'rerandomize': 'always'}
) )
for j, user in enumerate(self.users): for j, user in enumerate(self.users):
StudentModuleFactory.create( StudentModuleFactory.create(
grade=1 if i < j else 0, 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, student=user,
course_id=self.course.id, course_id=self.course.id,
module_state_key=Location(item.location).url(), module_state_key=Location(item.location).url(),
...@@ -98,36 +96,13 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -98,36 +96,13 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
max_grade = prob_grade_distrib[problem]['max_grade'] max_grade = prob_grade_distrib[problem]['max_grade']
self.assertEquals(1, 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): def test_get_sequential_open_distibution(self):
sequential_open_distrib = get_sequential_open_distrib(self.course.id) sequential_open_distrib = get_sequential_open_distrib(self.course.id)
for problem in sequential_open_distrib: for problem in sequential_open_distrib:
num_students = sequential_open_distrib[problem] num_students = sequential_open_distrib[problem]
self.assertEquals(USER_COUNT, num_students) 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): def test_get_problemset_grade_distrib(self):
...@@ -144,10 +119,8 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -144,10 +119,8 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
sum_attempts += item[1] sum_attempts += item[1]
self.assertEquals(USER_COUNT, sum_attempts) 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) d3_data = get_d3_problem_grade_distribution(self.course.id)
for data in d3_data: for data in d3_data:
for stack_data in data['data']: for stack_data in data['data']:
...@@ -156,54 +129,51 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -156,54 +129,51 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
sum_values += problem['value'] sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values) 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): def test_get_d3_sequential_open_distrib(self):
d3_data = get_d3_sequential_open_distribution(self.course.id) d3_data = get_d3_sequential_open_distribution(self.course.id)
for data in d3_data: for data in d3_data:
for stack_data in data['data']: for stack_data in data['data']:
for problem in stack_data['stackData']: for problem in stack_data['stackData']:
value = problem['value'] value = problem['value']
self.assertEquals(0, value) self.assertEquals(0, value)
def test_get_d3_section_grade_distrib(self): def test_get_d3_section_grade_distrib(self):
d3_data = get_d3_section_grade_distribution(self.course.id, 0) d3_data = get_d3_section_grade_distribution(self.course.id, 0)
for stack_data in d3_data: for stack_data in d3_data:
sum_values = 0 sum_values = 0
for problem in stack_data['stackData']: for problem in stack_data['stackData']:
sum_values += problem['value'] sum_values += problem['value']
self.assertEquals(USER_COUNT, sum_values) self.assertEquals(USER_COUNT, sum_values)
def test_get_section_display_name(self): def test_get_section_display_name(self):
section_display_name = get_section_display_name(self.course.id) section_display_name = get_section_display_name(self.course.id)
self.assertMultiLineEqual(section_display_name[0], 'test factory section') self.assertMultiLineEqual(section_display_name[0], 'test factory section')
def test_get_array_section_has_problem(self): def test_get_array_section_has_problem(self):
b_section_has_problem = get_array_section_has_problem(self.course.id) 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) 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 ...@@ -5,19 +5,21 @@ from django.test.client import RequestFactory
from django.utils import simplejson from django.utils import simplejson
from class_dashboard import views from class_dashboard import views
from student.tests.factories import AdminFactory
class TestViews(TestCase): class TestViews(TestCase):
def setUp(self): def setUp(self):
self.request_factory = RequestFactory() self.request_factory = RequestFactory()
self.request = self.request_factory.get('') self.request = self.request_factory.get('')
self.request.user = None self.request.user = None
self.simple_data = {'test': 'test'} 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') @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 Test returns proper value when have proper access
""" """
...@@ -25,23 +27,20 @@ class TestViews(TestCase): ...@@ -25,23 +27,20 @@ class TestViews(TestCase):
data_method.return_value = self.simple_data 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) self.assertEqual(simplejson.dumps(self.simple_data), response.content)
@patch('class_dashboard.dashboard_data.get_d3_problem_grade_distribution') @patch('class_dashboard.dashboard_data.get_d3_problem_grade_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class') @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 has_access.return_value = False
response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1')
data_method.return_value = self.simple_data
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
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.dashboard_data.get_d3_sequential_open_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class') @patch('class_dashboard.views.has_instructor_access_for_class')
...@@ -57,6 +56,17 @@ class TestViews(TestCase): ...@@ -57,6 +56,17 @@ class TestViews(TestCase):
self.assertEqual(simplejson.dumps(self.simple_data), response.content) 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.dashboard_data.get_d3_section_grade_distribution')
@patch('class_dashboard.views.has_instructor_access_for_class') @patch('class_dashboard.views.has_instructor_access_for_class')
def test_section_problem_grade_distribution_has_access(self, has_access, data_method): def test_section_problem_grade_distribution_has_access(self, has_access, data_method):
...@@ -70,3 +80,14 @@ class TestViews(TestCase): ...@@ -70,3 +80,14 @@ class TestViews(TestCase):
response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1') response = views.section_problem_grade_distribution(self.request, 'test/test/test', '1')
self.assertEqual(simplejson.dumps(self.simple_data), response.content) 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): ...@@ -16,28 +16,32 @@ def has_instructor_access_for_class(user, course_id):
""" """
course = get_course_with_access(user, course_id, 'staff', depth=None) course = get_course_with_access(user, course_id, 'staff', depth=None)
return has_access(user, course, 'instructor')
#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 # def all_problem_attempt_distribution(request, course_id):
# """
`course_id` the course ID for the course interested in # Creates a json with the attempt distribution for all the problems in the course.
#
Returns the format in dashboard_data.get_d3_problem_attempt_distribution # `request` django request
""" #
json = {} # `course_id` the course ID for the course interested in
#
# Only instructor for this particular course can request this information # Returns the format in dashboard_data.get_d3_problem_attempt_distribution
if has_instructor_access_for_class(request.user, course_id): # """
json = dashboard_data.get_d3_problem_attempt_distribution(course_id) # json = {}
else: #
json = {'error': "Access Denied: User does not have access to this course's data"} # # Only instructor for this particular course can request this information
# if has_instructor_access_for_class(request.user, course_id):
return HttpResponse(simplejson.dumps(json), mimetype="application/json") # 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): def all_sequential_open_distribution(request, course_id):
......
...@@ -25,6 +25,8 @@ from django_comment_client.utils import has_forum_access ...@@ -25,6 +25,8 @@ from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment from student.models import CourseEnrollment
from bulk_email.models import CourseAuthorization from bulk_email.models import CourseAuthorization
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
...@@ -50,8 +52,9 @@ def instructor_dashboard_2(request, course_id): ...@@ -50,8 +52,9 @@ def instructor_dashboard_2(request, course_id):
_section_course_info(course_id, access), _section_course_info(course_id, access),
_section_membership(course_id, access), _section_membership(course_id, access),
_section_student_admin(course_id, access), _section_student_admin(course_id, access),
_section_data_download(course_id), _section_data_download(course_id, access),
_section_analytics(course_id), _section_analytics(course_id, access),
# _section_metrics(course_id, access),
] ]
# Gate access to course email by feature flag & by course-specific authorization # Gate access to course email by feature flag & by course-specific authorization
...@@ -159,14 +162,16 @@ def _section_student_admin(course_id, access): ...@@ -159,14 +162,16 @@ def _section_student_admin(course_id, access):
return section_data return section_data
def _section_data_download(course_id): def _section_data_download(course_id, access):
""" Provide data for the corresponding dashboard section """ """ Provide data for the corresponding dashboard section """
section_data = { section_data = {
'section_key': 'data_download', 'section_key': 'data_download',
'section_display_name': _('Data Download'), 'section_display_name': _('Data Download'),
'access': access,
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}), '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_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}), '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 return section_data
...@@ -187,12 +192,25 @@ def _section_send_email(course_id, access, course): ...@@ -187,12 +192,25 @@ def _section_send_email(course_id, access, course):
return section_data return section_data
def _section_analytics(course_id): def _section_analytics(course_id, access):
""" Provide data for the corresponding dashboard section """ """ Provide data for the corresponding dashboard section """
section_data = { section_data = {
'section_key': 'analytics', 'section_key': 'analytics',
'section_display_name': _('Analytics'), 'section_display_name': _('Analytics'),
'access': access,
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}), 'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}),
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}), 'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
} }
return section_data 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): ...@@ -821,8 +821,9 @@ def instructor_dashboard(request, course_id):
# Metrics # Metrics
metrics_results = {} metrics_results = {}
if settings.MITX_FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics': # if settings.MITX_FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics':
metrics_results['attempts_timestamp'] = dashboard_data.get_last_populate(course_id, "studentmoduleexpand") 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_display_name'] = dashboard_data.get_section_display_name(course_id)
metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(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): ...@@ -874,35 +875,35 @@ def instructor_dashboard(request, course_id):
#---------------------------------------- #----------------------------------------
# context for rendering # context for rendering
context = {
'course': course, context = {'course': course,
'staff_access': True, 'staff_access': True,
'admin_access': request.user.is_staff, 'admin_access': request.user.is_staff,
'instructor_access': instructor_access, 'instructor_access': instructor_access,
'forum_admin_access': forum_admin_access, 'forum_admin_access': forum_admin_access,
'datatable': datatable, 'datatable': datatable,
'course_stats': course_stats, 'course_stats': course_stats,
'msg': msg, 'msg': msg,
'modeflag': {idash_mode: 'selectedmode'}, 'modeflag': {idash_mode: 'selectedmode'},
'studio_url': studio_url, 'studio_url': studio_url,
'to_option': email_to_option, # email 'to_option': email_to_option, # email
'subject': email_subject, # email 'subject': email_subject, # email
'editor': email_editor, # email 'editor': email_editor, # email
'email_msg': email_msg, # email 'email_msg': email_msg, # email
'show_email_tab': show_email_tab, # email 'show_email_tab': show_email_tab, # email
'problems': problems, # psychometrics 'problems': problems, # psychometrics
'plots': plots, # psychometrics 'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location), 'course_errors': modulestore().get_item_errors(course.location),
'instructor_tasks': instructor_tasks, 'instructor_tasks': instructor_tasks,
'offline_grade_log': offline_grades_available(course_id), 'offline_grade_log': offline_grades_available(course_id),
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'analytics_results': analytics_results, 'analytics_results': analytics_results,
'disable_buttons': disable_buttons, 'disable_buttons': disable_buttons,
'metrics_results': metrics_results, 'metrics_results': metrics_results,
} }
if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id}) context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id})
......
...@@ -1064,9 +1064,6 @@ VERIFY_STUDENT = { ...@@ -1064,9 +1064,6 @@ VERIFY_STUDENT = {
"DAYS_GOOD_FOR" : 365, # How many days is a verficiation good for? "DAYS_GOOD_FOR" : 365, # How many days is a verficiation good for?
} }
########################## QUERYABLE TABLES ########################
INSTALLED_APPS += ('queryable_student_module',)
########################## CLASS DASHBOARD ######################## ########################## CLASS DASHBOARD ########################
INSTALLED_APPS += ('class_dashboard',) INSTALLED_APPS += ('class_dashboard',)
MITX_FEATURES['CLASS_DASHBOARD'] = False MITX_FEATURES['CLASS_DASHBOARD'] = False
......
...@@ -167,6 +167,9 @@ setup_instructor_dashboard_sections = (idash_content) -> ...@@ -167,6 +167,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
, ,
constructor: window.InstructorDashboard.sections.Analytics constructor: window.InstructorDashboard.sections.Analytics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#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}) -> 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 () { ...@@ -29,6 +29,8 @@ $(function () {
barGraphOpened.scale.stackColor.range(["#555555","#555555"]); barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
barGraphOpened.drawGraph(); barGraphOpened.drawGraph();
$('svg').siblings('.loading').remove();
} }
i+=1; i+=1;
...@@ -57,36 +59,8 @@ $(function () { ...@@ -57,36 +59,8 @@ $(function () {
barGraphGrade.legend.width += 2; barGraphGrade.legend.width += 2;
barGraphGrade.drawGraph(); 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; i+=1;
......
...@@ -105,6 +105,7 @@ textarea { ...@@ -105,6 +105,7 @@ textarea {
width: 100%; width: 100%;
float: left; float: left;
clear: both; clear: both;
margin-top: 25px;
} }
.metrics-left { .metrics-left {
position: relative; position: relative;
...@@ -132,6 +133,22 @@ textarea { ...@@ -132,6 +133,22 @@ textarea {
fill: white; 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> </style>
<script language="JavaScript" type="text/javascript"> <script language="JavaScript" type="text/javascript">
...@@ -697,6 +714,8 @@ function goto( mode) ...@@ -697,6 +714,8 @@ function goto( mode)
</script> </script>
<div id="metrics"></div> <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'])): %for i in range(0,len(metrics_results['section_display_name'])):
<div class="metrics-container" id="metrics_section_${i}"> <div class="metrics-container" id="metrics_section_${i}">
...@@ -704,17 +723,14 @@ function goto( mode) ...@@ -704,17 +723,14 @@ function goto( mode)
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div> <div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
<div class="metrics-left" id="metric_opened_${i}"> <div class="metrics-left" id="metric_opened_${i}">
<h3>Count of Students Opened a Subsection</h3> <h3>Count of Students Opened a Subsection</h3>
<p class="loading"><i class="icon-spinner icon-spin icon-large"></i> Loading...</p>
</div> </div>
<div class="metrics-right" id="metric_grade_${i}"> <div class="metrics-right" id="metric_grade_${i}">
<h3>Grade Distribution per Problem</h3> <h3>Grade Distribution per Problem</h3>
%if not metrics_results['section_has_problem'][i]: %if not metrics_results['section_has_problem'][i]:
<p>${_("There are no problems in this section.")}</p> <p>${_("There are no problems in this section.")}</p>
%endif %else:
</div> <p class="loading"><i class="icon-spinner icon-spin icon-large"></i> Loading...</p>
<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>
%endif %endif
</div> </div>
</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 ...@@ -371,8 +371,6 @@ if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR
if settings.MITX_FEATURES.get('CLASS_DASHBOARD'): if settings.MITX_FEATURES.get('CLASS_DASHBOARD'):
urlpatterns += ( urlpatterns += (
# Json request data for metrics for entire course # 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$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distribution$',
'class_dashboard.views.all_sequential_open_distribution', name="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$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
...@@ -383,7 +381,6 @@ if settings.MITX_FEATURES.get('CLASS_DASHBOARD'): ...@@ -383,7 +381,6 @@ if settings.MITX_FEATURES.get('CLASS_DASHBOARD'):
'class_dashboard.views.section_problem_grade_distribution', name="section_problem_grade_distribution"), 'class_dashboard.views.section_problem_grade_distribution', name="section_problem_grade_distribution"),
) )
if settings.ENABLE_JASMINE: if settings.ENABLE_JASMINE:
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) 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