Merge pull request #23097 from edx/diana/remove-class-dashboard
Remove class_dashboard.
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
init.py file for class_dashboard
|
||||
"""
|
||||
@@ -1,605 +0,0 @@
|
||||
"""
|
||||
Computes the data to display on the Instructor Dashboard
|
||||
"""
|
||||
|
||||
|
||||
import decimal
|
||||
import json
|
||||
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import ugettext as _
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from six import text_type
|
||||
|
||||
from lms.djangoapps.courseware import models
|
||||
from lms.djangoapps.instructor_analytics.csvs import create_csv_response
|
||||
from util.json_request import JsonResponse
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from openedx.core.lib.grade_utils import round_away_from_zero
|
||||
|
||||
# Used to limit the length of list displayed to the screen.
|
||||
MAX_SCREEN_LIST_LENGTH = 250
|
||||
|
||||
|
||||
def get_problem_grade_distribution(course_id):
|
||||
"""
|
||||
Returns the grade distribution per problem for the course
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Output is 2 dicts:
|
||||
'prob-grade_distrib' where the key is the problem 'module_id' and the value is a dict with:
|
||||
'max_grade' - max grade for this problem
|
||||
'grade_distrib' - array of tuples (`grade`,`count`).
|
||||
'total_student_count' where the key is problem 'module_id' and the value is number of students
|
||||
attempting the problem
|
||||
"""
|
||||
|
||||
# Aggregate query on studentmodule table for grade data for all problems in course
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
grade__isnull=False,
|
||||
module_type__exact="problem",
|
||||
).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
|
||||
|
||||
prob_grade_distrib = {}
|
||||
total_student_count = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
curr_problem = row['module_state_key'].map_into_course(course_id)
|
||||
|
||||
# Build set of grade distributions for each problem that has student responses
|
||||
if curr_problem in prob_grade_distrib:
|
||||
prob_grade_distrib[curr_problem]['grade_distrib'].append((row['grade'], row['count_grade']))
|
||||
|
||||
if (prob_grade_distrib[curr_problem]['max_grade'] != row['max_grade']) and \
|
||||
(prob_grade_distrib[curr_problem]['max_grade'] < row['max_grade']):
|
||||
prob_grade_distrib[curr_problem]['max_grade'] = row['max_grade']
|
||||
|
||||
else:
|
||||
prob_grade_distrib[curr_problem] = {
|
||||
'max_grade': row['max_grade'],
|
||||
'grade_distrib': [(row['grade'], row['count_grade'])]
|
||||
}
|
||||
|
||||
# Build set of total students attempting each problem
|
||||
total_student_count[curr_problem] = total_student_count.get(curr_problem, 0) + row['count_grade']
|
||||
|
||||
return prob_grade_distrib, total_student_count
|
||||
|
||||
|
||||
def get_sequential_open_distrib(course_id):
|
||||
"""
|
||||
Returns the number of students that opened each subsection/sequential of the course
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Outputs a dict mapping the 'module_id' to the number of students that have opened that subsection/sequential.
|
||||
"""
|
||||
|
||||
# Aggregate query on studentmodule table for "opening a subsection" data
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
module_type__exact="sequential",
|
||||
).values('module_state_key').annotate(count_sequential=Count('module_state_key'))
|
||||
|
||||
# Build set of "opened" data for each subsection that has "opened" data
|
||||
sequential_open_distrib = {}
|
||||
for row in db_query:
|
||||
row_loc = row['module_state_key'].map_into_course(course_id)
|
||||
sequential_open_distrib[row_loc] = row['count_sequential']
|
||||
|
||||
return sequential_open_distrib
|
||||
|
||||
|
||||
def get_problem_set_grade_distrib(course_id, problem_set):
|
||||
"""
|
||||
Returns the grade distribution for the problems specified in `problem_set`.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
`problem_set` an array of UsageKeys representing problem module_id's.
|
||||
|
||||
Requests from the database the a count of each grade for each problem in the `problem_set`.
|
||||
|
||||
Returns a dict, where the key is the problem 'module_id' and the value is a dict with two parts:
|
||||
'max_grade' - the maximum grade possible for the course
|
||||
'grade_distrib' - array of tuples (`grade`,`count`) ordered by `grade`
|
||||
"""
|
||||
|
||||
# Aggregate query on studentmodule table for grade data for set of problems in course
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
grade__isnull=False,
|
||||
module_type__exact="problem",
|
||||
module_state_key__in=problem_set,
|
||||
).values(
|
||||
'module_state_key',
|
||||
'grade',
|
||||
'max_grade',
|
||||
).annotate(count_grade=Count('grade')).order_by('module_state_key', 'grade')
|
||||
|
||||
prob_grade_distrib = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
row_loc = row['module_state_key'].map_into_course(course_id)
|
||||
if row_loc not in prob_grade_distrib:
|
||||
prob_grade_distrib[row_loc] = {
|
||||
'max_grade': 0,
|
||||
'grade_distrib': [],
|
||||
}
|
||||
|
||||
curr_grade_distrib = prob_grade_distrib[row_loc]
|
||||
curr_grade_distrib['grade_distrib'].append((row['grade'], row['count_grade']))
|
||||
|
||||
if curr_grade_distrib['max_grade'] < row['max_grade']:
|
||||
curr_grade_distrib['max_grade'] = row['max_grade']
|
||||
|
||||
return prob_grade_distrib
|
||||
|
||||
|
||||
def get_d3_problem_grade_distrib(course_id):
|
||||
"""
|
||||
Returns problem grade distribution information for each section, data already in format for d3 function.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
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 d3_stacked_bar_graph function of the grade distribution for that problem
|
||||
"""
|
||||
|
||||
prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id)
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to problems
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
# Iterate through sections, subsections, units, problems
|
||||
for section in course.get_children():
|
||||
curr_section = {}
|
||||
curr_section['display_name'] = own_metadata(section).get('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():
|
||||
|
||||
# Student data is at the problem level
|
||||
if child.location.block_type == 'problem':
|
||||
c_problem += 1
|
||||
stack_data = []
|
||||
|
||||
# Construct label to display for this problem
|
||||
label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem)
|
||||
|
||||
# Only problems in prob_grade_distrib have had a student submission.
|
||||
if child.location in prob_grade_distrib:
|
||||
|
||||
# Get max_grade, grade_distribution for this problem
|
||||
problem_info = prob_grade_distrib[child.location]
|
||||
|
||||
# Get problem_name for tooltip
|
||||
problem_name = own_metadata(child).get('display_name', '')
|
||||
|
||||
# Compute percent of this grade over max_grade
|
||||
max_grade = float(problem_info['max_grade'])
|
||||
for (grade, count_grade) in problem_info['grade_distrib']:
|
||||
percent = 0.0
|
||||
if max_grade > 0:
|
||||
percent = round_away_from_zero((grade * 100.0) / max_grade, 1)
|
||||
|
||||
# Compute percent of students with this grade
|
||||
student_count_percent = 0
|
||||
if total_student_count.get(child.location, 0) > 0:
|
||||
student_count_percent = count_grade * 100 / total_student_count[child.location]
|
||||
|
||||
# Tooltip parameters for problem in grade distribution view
|
||||
tooltip = {
|
||||
'type': 'problem',
|
||||
'label': label,
|
||||
'problem_name': problem_name,
|
||||
'count_grade': count_grade,
|
||||
'percent': percent,
|
||||
'grade': grade,
|
||||
'max_grade': max_grade,
|
||||
'student_count_percent': student_count_percent,
|
||||
}
|
||||
|
||||
# Construct data to be sent to d3
|
||||
stack_data.append({
|
||||
'color': percent,
|
||||
'value': count_grade,
|
||||
'tooltip': tooltip,
|
||||
'module_url': text_type(child.location),
|
||||
})
|
||||
|
||||
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_distrib(course_id):
|
||||
"""
|
||||
Returns how many students opened a sequential/subsection for each section, data already in format for d3 function.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Returns an array in the order of the sections and each dict has:
|
||||
'display_name' - display name for the section
|
||||
'data' - data for the d3_stacked_bar_graph function of how many students opened each sequential/subsection
|
||||
"""
|
||||
sequential_open_distrib = get_sequential_open_distrib(course_id)
|
||||
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to subsection
|
||||
course = modulestore().get_course(course_id, depth=2)
|
||||
|
||||
# Iterate through sections, subsections
|
||||
for section in course.get_children():
|
||||
curr_section = {}
|
||||
curr_section['display_name'] = own_metadata(section).get('display_name', '')
|
||||
data = []
|
||||
c_subsection = 0
|
||||
|
||||
# Construct data for each subsection to be sent to d3
|
||||
for subsection in section.get_children():
|
||||
c_subsection += 1
|
||||
subsection_name = own_metadata(subsection).get('display_name', '')
|
||||
|
||||
num_students = 0
|
||||
if subsection.location in sequential_open_distrib:
|
||||
num_students = sequential_open_distrib[subsection.location]
|
||||
|
||||
stack_data = []
|
||||
|
||||
# Tooltip parameters for subsection in open_distribution view
|
||||
tooltip = {
|
||||
'type': 'subsection',
|
||||
'num_students': num_students,
|
||||
'subsection_num': c_subsection,
|
||||
'subsection_name': subsection_name
|
||||
}
|
||||
|
||||
stack_data.append({
|
||||
'color': 0,
|
||||
'value': num_students,
|
||||
'tooltip': tooltip,
|
||||
'module_url': text_type(subsection.location),
|
||||
})
|
||||
subsection = {
|
||||
'xValue': u"SS {0}".format(c_subsection),
|
||||
'stackData': stack_data,
|
||||
}
|
||||
data.append(subsection)
|
||||
|
||||
curr_section['data'] = data
|
||||
d3_data.append(curr_section)
|
||||
|
||||
return d3_data
|
||||
|
||||
|
||||
def get_d3_section_grade_distrib(course_id, section):
|
||||
"""
|
||||
Returns the grade distribution for the problems in the `section` section in a format for the d3 code.
|
||||
|
||||
`course_id` a string that is the course's ID.
|
||||
|
||||
`section` an int that is a zero-based index into the course's list of sections.
|
||||
|
||||
Navigates to the section specified to find all the problems associated with that section and then finds the grade
|
||||
distribution for those problems. Finally returns an object formated the way the d3_stacked_bar_graph.js expects its
|
||||
data object to be in.
|
||||
|
||||
If this is requested multiple times quickly for the same course, it is better to call
|
||||
get_d3_problem_grade_distrib and pick out the sections of interest.
|
||||
|
||||
Returns an array of dicts with the following keys (taken from d3_stacked_bar_graph.js's documentation)
|
||||
'xValue' - Corresponding value for the x-axis
|
||||
'stackData' - Array of objects with key, value pairs that represent a bar:
|
||||
'color' - Defines what "color" the bar will map to
|
||||
'value' - Maps to the height of the bar, along the y-axis
|
||||
'tooltip' - (Optional) Text to display on mouse hover
|
||||
"""
|
||||
|
||||
# Retrieve course object down to problems
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
problem_set = []
|
||||
problem_info = {}
|
||||
c_subsection = 0
|
||||
for subsection in course.get_children()[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.block_type == 'problem':
|
||||
c_problem += 1
|
||||
problem_set.append(child.location)
|
||||
problem_info[child.location] = {
|
||||
'id': text_type(child.location),
|
||||
'x_value': "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem),
|
||||
'display_name': own_metadata(child).get('display_name', ''),
|
||||
}
|
||||
|
||||
# Retrieve grade distribution for these problems
|
||||
grade_distrib = get_problem_set_grade_distrib(course_id, problem_set)
|
||||
|
||||
d3_data = []
|
||||
|
||||
# Construct data for each problem to be sent to d3
|
||||
for problem in problem_set:
|
||||
stack_data = []
|
||||
|
||||
if problem in grade_distrib: # Some problems have no data because students have not tried them yet.
|
||||
max_grade = float(grade_distrib[problem]['max_grade'])
|
||||
for (grade, count_grade) in grade_distrib[problem]['grade_distrib']:
|
||||
percent = 0.0
|
||||
if max_grade > 0:
|
||||
percent = round_away_from_zero((grade * 100.0) / max_grade, 1)
|
||||
|
||||
# Construct tooltip for problem in grade distibution view
|
||||
tooltip = {
|
||||
'type': 'problem',
|
||||
'problem_info_x': problem_info[problem]['x_value'],
|
||||
'count_grade': count_grade,
|
||||
'percent': percent,
|
||||
'problem_info_n': problem_info[problem]['display_name'],
|
||||
'grade': grade,
|
||||
'max_grade': max_grade,
|
||||
}
|
||||
|
||||
stack_data.append({
|
||||
'color': percent,
|
||||
'value': count_grade,
|
||||
'tooltip': tooltip,
|
||||
})
|
||||
|
||||
d3_data.append({
|
||||
'xValue': problem_info[problem]['x_value'],
|
||||
'stackData': stack_data,
|
||||
})
|
||||
|
||||
return d3_data
|
||||
|
||||
|
||||
def get_section_display_name(course_id):
|
||||
"""
|
||||
Returns an array of the display names for each section in the course.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
The ith string in the array is the display name of the ith section in the course.
|
||||
"""
|
||||
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
section_display_name = [""] * len(course.get_children())
|
||||
i = 0
|
||||
for section in course.get_children():
|
||||
section_display_name[i] = own_metadata(section).get('display_name', '')
|
||||
i += 1
|
||||
|
||||
return section_display_name
|
||||
|
||||
|
||||
def get_array_section_has_problem(course_id):
|
||||
"""
|
||||
Returns an array of true/false whether each section has problems.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
The ith value in the array is true if the ith section in the course contains problems and false otherwise.
|
||||
"""
|
||||
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
b_section_has_problem = [False] * len(course.get_children())
|
||||
i = 0
|
||||
for section in course.get_children():
|
||||
for subsection in section.get_children():
|
||||
for unit in subsection.get_children():
|
||||
for child in unit.get_children():
|
||||
if child.location.block_type == 'problem':
|
||||
b_section_has_problem[i] = True
|
||||
break # out of child loop
|
||||
if b_section_has_problem[i]:
|
||||
break # out of unit loop
|
||||
if b_section_has_problem[i]:
|
||||
break # out of subsection loop
|
||||
|
||||
i += 1
|
||||
|
||||
return b_section_has_problem
|
||||
|
||||
|
||||
def get_students_opened_subsection(request, csv=False):
|
||||
"""
|
||||
Get a list of students that opened a particular subsection.
|
||||
If 'csv' is False, returns a dict of student's name: username.
|
||||
|
||||
If 'csv' is True, returns a header array, and an array of arrays in the format:
|
||||
student names, usernames for CSV download.
|
||||
"""
|
||||
module_state_key = BlockUsageLocator.from_string(request.GET.get('module_id'))
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
# Query for "opened a subsection" students
|
||||
students = models.StudentModule.objects.select_related('student').filter(
|
||||
module_state_key__exact=module_state_key,
|
||||
module_type__exact='sequential',
|
||||
).values('student__username', 'student__profile__name').order_by('student__profile__name')
|
||||
|
||||
results = []
|
||||
if not csv:
|
||||
# Restrict screen list length
|
||||
# Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH
|
||||
# without doing another select.
|
||||
for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]:
|
||||
results.append({
|
||||
'name': student['student__profile__name'],
|
||||
'username': student['student__username'],
|
||||
})
|
||||
|
||||
max_exceeded = False
|
||||
if len(results) > MAX_SCREEN_LIST_LENGTH:
|
||||
# Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH
|
||||
del results[-1]
|
||||
max_exceeded = True
|
||||
response_payload = {
|
||||
'results': results,
|
||||
'max_exceeded': max_exceeded,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
else:
|
||||
tooltip = request.GET.get('tooltip')
|
||||
|
||||
# Subsection name is everything after 3rd space in tooltip
|
||||
filename = sanitize_filename(' '.join(tooltip.split(' ')[3:]))
|
||||
|
||||
header = [_("Name"), _("Username")]
|
||||
for student in students:
|
||||
results.append([student['student__profile__name'], student['student__username']])
|
||||
|
||||
response = create_csv_response(filename, header, results)
|
||||
return response
|
||||
|
||||
|
||||
def get_students_problem_grades(request, csv=False):
|
||||
"""
|
||||
Get a list of students and grades for a particular problem.
|
||||
If 'csv' is False, returns a dict of student's name: username: grade: percent.
|
||||
|
||||
If 'csv' is True, returns a header array, and an array of arrays in the format:
|
||||
student names, usernames, grades, percents for CSV download.
|
||||
"""
|
||||
module_state_key = BlockUsageLocator.from_string(request.GET.get('module_id'))
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
# Query for "problem grades" students
|
||||
students = models.StudentModule.objects.select_related('student').filter(
|
||||
module_state_key=module_state_key,
|
||||
module_type__exact='problem',
|
||||
grade__isnull=False,
|
||||
).values('student__username', 'student__profile__name', 'grade', 'max_grade').order_by('student__profile__name')
|
||||
|
||||
results = []
|
||||
if not csv:
|
||||
# Restrict screen list length
|
||||
# Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH
|
||||
# without doing another select.
|
||||
for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]:
|
||||
student_dict = {
|
||||
'name': student['student__profile__name'],
|
||||
'username': student['student__username'],
|
||||
'grade': student['grade'],
|
||||
}
|
||||
|
||||
student_dict['percent'] = 0
|
||||
if student['max_grade'] > 0:
|
||||
student_dict['percent'] = round_away_from_zero(student['grade'] * 100 / student['max_grade'])
|
||||
results.append(student_dict)
|
||||
|
||||
max_exceeded = False
|
||||
if len(results) > MAX_SCREEN_LIST_LENGTH:
|
||||
# Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH
|
||||
del results[-1]
|
||||
max_exceeded = True
|
||||
|
||||
response_payload = {
|
||||
'results': results,
|
||||
'max_exceeded': max_exceeded,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
else:
|
||||
tooltip = request.GET.get('tooltip')
|
||||
filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')])
|
||||
|
||||
header = [_("Name"), _("Username"), _("Grade"), _("Percent")]
|
||||
for student in students:
|
||||
percent = 0
|
||||
if student['max_grade'] > 0:
|
||||
percent = round_away_from_zero((student['grade'] * 100 / student['max_grade']), 1)
|
||||
results.append([student['student__profile__name'], student['student__username'], student['grade'], percent])
|
||||
|
||||
response = create_csv_response(filename, header, results)
|
||||
return response
|
||||
|
||||
|
||||
def post_metrics_data_csv(request):
|
||||
"""
|
||||
Generate a list of opened subsections or problems for the entire course for CSV download.
|
||||
Returns a header array, and an array of arrays in the format:
|
||||
section, subsection, count of students for subsections
|
||||
or section, problem, name, count of students, percent of students, score for problems.
|
||||
"""
|
||||
|
||||
data = json.loads(request.POST['data'])
|
||||
sections = json.loads(data['sections'])
|
||||
tooltips = json.loads(data['tooltips'])
|
||||
course_id = data['course_id']
|
||||
data_type = data['data_type']
|
||||
|
||||
results = []
|
||||
if data_type == 'subsection':
|
||||
header = [_("Section"), _("Subsection"), _("Opened by this number of students")]
|
||||
filename = sanitize_filename(_('subsections') + '_' + course_id)
|
||||
elif data_type == 'problem':
|
||||
header = [
|
||||
_("Section"), _("Problem"), _("Name"), _("Count of Students"),
|
||||
_("Percent of Students"), _("Score"),
|
||||
]
|
||||
filename = sanitize_filename(_('problems') + '_' + course_id)
|
||||
|
||||
for index, section in enumerate(sections):
|
||||
results.append([section])
|
||||
|
||||
# tooltips array is array of dicts for subsections and
|
||||
# array of array of dicts for problems.
|
||||
if data_type == 'subsection':
|
||||
for tooltip_dict in tooltips[index]:
|
||||
num_students = tooltip_dict['num_students']
|
||||
subsection = tooltip_dict['subsection_name']
|
||||
# Append to results offsetting 1 column to the right.
|
||||
results.append(['', subsection, num_students])
|
||||
|
||||
elif data_type == 'problem':
|
||||
for tooltip in tooltips[index]:
|
||||
for tooltip_dict in tooltip:
|
||||
label = tooltip_dict['label']
|
||||
problem_name = tooltip_dict['problem_name']
|
||||
count_grade = tooltip_dict['count_grade']
|
||||
student_count_percent = tooltip_dict['student_count_percent']
|
||||
percent = tooltip_dict['percent']
|
||||
# Append to results offsetting 1 column to the right.
|
||||
results.append(['', label, problem_name, count_grade, student_count_percent, percent])
|
||||
|
||||
response = create_csv_response(filename, header, results)
|
||||
return response
|
||||
|
||||
|
||||
def sanitize_filename(filename):
|
||||
"""
|
||||
Utility function
|
||||
"""
|
||||
filename = filename.replace(" ", "_")
|
||||
filename = filename.encode('utf-8')
|
||||
filename = filename[0:25].decode('utf-8') + '.csv'
|
||||
return filename
|
||||
@@ -1,331 +0,0 @@
|
||||
"""
|
||||
Tests for class dashboard (Metrics tab in instructor dashboard)
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from mock import patch
|
||||
from six import text_type
|
||||
from six.moves import range
|
||||
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
from class_dashboard.dashboard_data import (
|
||||
get_array_section_has_problem,
|
||||
get_d3_problem_grade_distrib,
|
||||
get_d3_section_grade_distrib,
|
||||
get_d3_sequential_open_distrib,
|
||||
get_problem_grade_distribution,
|
||||
get_problem_set_grade_distrib,
|
||||
get_section_display_name,
|
||||
get_sequential_open_distrib,
|
||||
get_students_opened_subsection,
|
||||
get_students_problem_grades
|
||||
)
|
||||
from class_dashboard.views import has_instructor_access_for_class
|
||||
from lms.djangoapps.courseware.tests.factories import StudentModuleFactory
|
||||
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
USER_COUNT = 11
|
||||
|
||||
|
||||
class TestGetProblemGradeDistribution(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests related to class_dashboard/dashboard_data.py
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestGetProblemGradeDistribution, cls).setUpClass()
|
||||
cls.course = CourseFactory.create(
|
||||
display_name=u"test course omega \u03a9",
|
||||
)
|
||||
with cls.store.bulk_operations(cls.course.id, emit_signals=False):
|
||||
section = ItemFactory.create(
|
||||
parent_location=cls.course.location,
|
||||
category="chapter",
|
||||
display_name=u"test factory section omega \u03a9",
|
||||
)
|
||||
cls.sub_section = ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
category="sequential",
|
||||
display_name=u"test subsection omega \u03a9",
|
||||
)
|
||||
cls.unit = ItemFactory.create(
|
||||
parent_location=cls.sub_section.location,
|
||||
category="vertical",
|
||||
metadata={'graded': True, 'format': 'Homework'},
|
||||
display_name=u"test unit omega \u03a9",
|
||||
)
|
||||
cls.items = []
|
||||
for i in range(USER_COUNT - 1):
|
||||
item = ItemFactory.create(
|
||||
parent_location=cls.unit.location,
|
||||
category="problem",
|
||||
data=StringResponseXMLFactory().build_xml(answer='foo'),
|
||||
metadata={'rerandomize': 'always'},
|
||||
display_name=u"test problem omega \u03a9 " + str(i)
|
||||
)
|
||||
cls.items.append(item)
|
||||
cls.item = item
|
||||
|
||||
def setUp(self):
|
||||
super(TestGetProblemGradeDistribution, self).setUp()
|
||||
|
||||
self.request_factory = RequestFactory()
|
||||
self.instructor = AdminFactory.create()
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.attempts = 3
|
||||
self.users = [
|
||||
UserFactory.create(username="metric" + str(__))
|
||||
for __ in range(USER_COUNT)
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
for i, item in enumerate(self.items):
|
||||
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,
|
||||
student=user,
|
||||
course_id=self.course.id,
|
||||
module_state_key=item.location,
|
||||
state=json.dumps({'attempts': self.attempts}),
|
||||
)
|
||||
for j, user in enumerate(self.users):
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course.id,
|
||||
module_type='sequential',
|
||||
module_state_key=item.location,
|
||||
)
|
||||
|
||||
def test_get_problem_grade_distribution(self):
|
||||
|
||||
prob_grade_distrib, total_student_count = get_problem_grade_distribution(self.course.id)
|
||||
|
||||
for problem in prob_grade_distrib:
|
||||
max_grade = prob_grade_distrib[problem]['max_grade']
|
||||
self.assertEqual(1, max_grade)
|
||||
|
||||
for val in total_student_count.values():
|
||||
self.assertEqual(USER_COUNT, val)
|
||||
|
||||
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.assertEqual(USER_COUNT, num_students)
|
||||
|
||||
def test_get_problemset_grade_distrib(self):
|
||||
|
||||
prob_grade_distrib, __ = get_problem_grade_distribution(self.course.id)
|
||||
probset_grade_distrib = get_problem_set_grade_distrib(self.course.id, prob_grade_distrib)
|
||||
|
||||
for problem in probset_grade_distrib:
|
||||
max_grade = probset_grade_distrib[problem]['max_grade']
|
||||
self.assertEqual(1, max_grade)
|
||||
|
||||
grade_distrib = probset_grade_distrib[problem]['grade_distrib']
|
||||
sum_attempts = 0
|
||||
for item in grade_distrib:
|
||||
sum_attempts += item[1]
|
||||
self.assertEqual(USER_COUNT, sum_attempts)
|
||||
|
||||
def test_get_d3_problem_grade_distrib(self):
|
||||
|
||||
d3_data = get_d3_problem_grade_distrib(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.assertEqual(USER_COUNT, sum_values)
|
||||
|
||||
def test_get_d3_sequential_open_distrib(self):
|
||||
|
||||
d3_data = get_d3_sequential_open_distrib(self.course.id)
|
||||
|
||||
for data in d3_data:
|
||||
for stack_data in data['data']:
|
||||
for problem in stack_data['stackData']:
|
||||
value = problem['value']
|
||||
self.assertEqual(0, value)
|
||||
|
||||
def test_get_d3_section_grade_distrib(self):
|
||||
|
||||
d3_data = get_d3_section_grade_distrib(self.course.id, 0)
|
||||
|
||||
for stack_data in d3_data:
|
||||
sum_values = 0
|
||||
for problem in stack_data['stackData']:
|
||||
sum_values += problem['value']
|
||||
self.assertEqual(USER_COUNT, sum_values)
|
||||
|
||||
def test_get_students_problem_grades(self):
|
||||
|
||||
attributes = '?module_id=' + text_type(self.item.location)
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
response_content = json.loads(response.content.decode('utf-8'))['results']
|
||||
response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded']
|
||||
|
||||
self.assertEqual(USER_COUNT, len(response_content))
|
||||
self.assertEqual(False, response_max_exceeded)
|
||||
for item in response_content:
|
||||
if item['grade'] == 0:
|
||||
self.assertEqual(0, item['percent'])
|
||||
else:
|
||||
self.assertEqual(100, item['percent'])
|
||||
|
||||
def test_get_students_problem_grades_max(self):
|
||||
|
||||
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2):
|
||||
attributes = '?module_id=' + text_type(self.item.location)
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
response_results = json.loads(response.content.decode('utf-8'))['results']
|
||||
response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded']
|
||||
|
||||
# Only 2 students in the list and response_max_exceeded is True
|
||||
self.assertEqual(2, len(response_results))
|
||||
self.assertEqual(True, response_max_exceeded)
|
||||
|
||||
def test_get_students_problem_grades_csv(self):
|
||||
|
||||
tooltip = u'P1.2.1 Q1 - 3382 Students (100%: 1/1 questions)'
|
||||
attributes = '?module_id=' + text_type(self.item.location) + '&tooltip=' + tooltip + '&csv=true'
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
# Check header and a row for each student in csv response
|
||||
self.assertContains(response, '"Name","Username","Grade","Percent"')
|
||||
self.assertContains(response, '"metric0","0.0","0.0"')
|
||||
self.assertContains(response, '"metric1","0.0","0.0"')
|
||||
self.assertContains(response, '"metric2","0.0","0.0"')
|
||||
self.assertContains(response, '"metric3","0.0","0.0"')
|
||||
self.assertContains(response, '"metric4","0.0","0.0"')
|
||||
self.assertContains(response, '"metric5","0.0","0.0"')
|
||||
self.assertContains(response, '"metric6","0.0","0.0"')
|
||||
self.assertContains(response, '"metric7","0.0","0.0"')
|
||||
self.assertContains(response, '"metric8","0.0","0.0"')
|
||||
self.assertContains(response, '"metric9","0.0","0.0"')
|
||||
self.assertContains(response, '"metric10","1.0","100.0"')
|
||||
|
||||
def test_get_students_opened_subsection(self):
|
||||
|
||||
attributes = '?module_id=' + text_type(self.item.location)
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
response_results = json.loads(response.content.decode('utf-8'))['results']
|
||||
response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded']
|
||||
self.assertEqual(USER_COUNT, len(response_results))
|
||||
self.assertEqual(False, response_max_exceeded)
|
||||
|
||||
def test_get_students_opened_subsection_max(self):
|
||||
|
||||
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2):
|
||||
|
||||
attributes = '?module_id=' + text_type(self.item.location)
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
response_results = json.loads(response.content.decode('utf-8'))['results']
|
||||
response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded']
|
||||
|
||||
# Only 2 students in the list and response_max_exceeded is True
|
||||
self.assertEqual(2, len(response_results))
|
||||
self.assertEqual(True, response_max_exceeded)
|
||||
|
||||
def test_get_students_opened_subsection_csv(self):
|
||||
|
||||
tooltip = '4162 students opened Subsection 5: Relational Algebra Exercises'
|
||||
attributes = '?module_id=' + text_type(self.item.location) + '&tooltip=' + tooltip + '&csv=true'
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
self.assertContains(response, '"Name","Username"')
|
||||
# Check response contains 1 line for each user +1 for the header
|
||||
self.assertEqual(USER_COUNT + 1, len(response.content.splitlines()))
|
||||
|
||||
def test_post_metrics_data_subsections_csv(self):
|
||||
|
||||
url = reverse('post_metrics_data_csv')
|
||||
|
||||
sections = json.dumps(["Introduction"])
|
||||
tooltips = json.dumps([[{"subsection_name": "Pre-Course Survey", "subsection_num": 1, "type": "subsection", "num_students": 18963}]])
|
||||
course_id = self.course.id
|
||||
data_type = 'subsection'
|
||||
|
||||
data = json.dumps({'sections': sections,
|
||||
'tooltips': tooltips,
|
||||
'course_id': text_type(course_id),
|
||||
'data_type': data_type,
|
||||
})
|
||||
|
||||
response = self.client.post(url, {'data': data})
|
||||
# Check response contains 1 line for header, 1 line for Section and 1 line for Subsection
|
||||
self.assertEqual(3, len(response.content.splitlines()))
|
||||
|
||||
def test_post_metrics_data_problems_csv(self):
|
||||
|
||||
url = reverse('post_metrics_data_csv')
|
||||
|
||||
sections = json.dumps(["Introduction"])
|
||||
tooltips = json.dumps([[[
|
||||
{'student_count_percent': 0,
|
||||
'problem_name': 'Q1',
|
||||
'grade': 0,
|
||||
'percent': 0,
|
||||
'label': 'P1.2.1',
|
||||
'max_grade': 1,
|
||||
'count_grade': 26,
|
||||
'type': u'problem'},
|
||||
{'student_count_percent': 99,
|
||||
'problem_name': 'Q1',
|
||||
'grade': 1,
|
||||
'percent': 100,
|
||||
'label': 'P1.2.1',
|
||||
'max_grade': 1,
|
||||
'count_grade': 4763,
|
||||
'type': 'problem'},
|
||||
]]])
|
||||
course_id = self.course.id
|
||||
data_type = 'problem'
|
||||
|
||||
data = json.dumps({'sections': sections,
|
||||
'tooltips': tooltips,
|
||||
'course_id': text_type(course_id),
|
||||
'data_type': data_type,
|
||||
})
|
||||
|
||||
response = self.client.post(url, {'data': data})
|
||||
# Check response contains 1 line for header, 1 line for Sections and 2 lines for problems
|
||||
self.assertEqual(4, len(response.content.splitlines()))
|
||||
|
||||
def test_get_section_display_name(self):
|
||||
|
||||
section_display_name = get_section_display_name(self.course.id)
|
||||
self.assertMultiLineEqual(section_display_name[0], u"test factory section omega \u03a9")
|
||||
|
||||
def test_get_array_section_has_problem(self):
|
||||
|
||||
b_section_has_problem = get_array_section_has_problem(self.course.id)
|
||||
self.assertEqual(b_section_has_problem[0], True)
|
||||
|
||||
def test_has_instructor_access_for_class(self):
|
||||
"""
|
||||
Test for instructor access
|
||||
"""
|
||||
ret_val = bool(has_instructor_access_for_class(self.instructor, self.course.id))
|
||||
self.assertEqual(ret_val, True)
|
||||
@@ -1,113 +0,0 @@
|
||||
"""
|
||||
Tests for class dashboard (Metrics tab in instructor dashboard)
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
from mock import patch
|
||||
from six import text_type
|
||||
|
||||
from class_dashboard import views
|
||||
from student.tests.factories import AdminFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
class TestViews(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests related to class_dashboard/views.py
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestViews, self).setUp()
|
||||
|
||||
self.request_factory = RequestFactory()
|
||||
self.request = self.request_factory.get('')
|
||||
self.request.user = None
|
||||
self.simple_data = {'error': 'error'}
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_problem_grade_distribution_has_access(self, has_access):
|
||||
"""
|
||||
Test returns proper value when have proper access
|
||||
"""
|
||||
has_access.return_value = True
|
||||
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual(json.dumps(self.simple_data), response.content.decode('utf-8'))
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_problem_grade_distribution_no_access(self, has_access):
|
||||
"""
|
||||
Test for no access
|
||||
"""
|
||||
has_access.return_value = False
|
||||
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual(
|
||||
"{\"error\": \"Access Denied: User does not have access to this course\'s data\"}",
|
||||
response.content.decode('utf-8')
|
||||
)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_sequential_open_distribution_has_access(self, has_access):
|
||||
"""
|
||||
Test returns proper value when have proper access
|
||||
"""
|
||||
has_access.return_value = True
|
||||
response = views.all_sequential_open_distrib(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual(json.dumps(self.simple_data), response.content.decode('utf-8'))
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_sequential_open_distribution_no_access(self, has_access):
|
||||
"""
|
||||
Test for no access
|
||||
"""
|
||||
has_access.return_value = False
|
||||
response = views.all_sequential_open_distrib(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual(
|
||||
"{\"error\": \"Access Denied: User does not have access to this course\'s data\"}",
|
||||
response.content.decode('utf-8')
|
||||
)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_section_problem_grade_distribution_has_access(self, has_access):
|
||||
"""
|
||||
Test returns proper value when have proper access
|
||||
"""
|
||||
has_access.return_value = True
|
||||
response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1')
|
||||
|
||||
self.assertEqual(json.dumps(self.simple_data), response.content.decode('utf-8'))
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_section_problem_grade_distribution_no_access(self, has_access):
|
||||
"""
|
||||
Test for no access
|
||||
"""
|
||||
has_access.return_value = False
|
||||
response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1')
|
||||
|
||||
self.assertEqual(
|
||||
"{\"error\": \"Access Denied: User does not have access to this course\'s data\"}",
|
||||
response.content.decode('utf-8')
|
||||
)
|
||||
|
||||
def test_sending_deprecated_id(self):
|
||||
|
||||
course = CourseFactory.create()
|
||||
instructor = AdminFactory.create()
|
||||
self.request.user = instructor
|
||||
|
||||
response = views.all_sequential_open_distrib(self.request, text_type(course.id))
|
||||
self.assertEqual('[]', response.content.decode('utf-8'))
|
||||
|
||||
response = views.all_problem_grade_distribution(self.request, text_type(course.id))
|
||||
self.assertEqual('[]', response.content.decode('utf-8'))
|
||||
|
||||
response = views.section_problem_grade_distrib(self.request, text_type(course.id), 'no section')
|
||||
self.assertEqual('{"error": "error"}', response.content.decode('utf-8'))
|
||||
@@ -1,37 +0,0 @@
|
||||
"""
|
||||
Class Dashboard API endpoint urls.
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
import class_dashboard.dashboard_data
|
||||
import class_dashboard.views
|
||||
|
||||
COURSE_ID_PATTERN = settings.COURSE_ID_PATTERN
|
||||
|
||||
urlpatterns = [
|
||||
# Json request data for metrics for entire course
|
||||
url(r'^{}/all_sequential_open_distrib$'.format(settings.COURSE_ID_PATTERN),
|
||||
class_dashboard.views.all_sequential_open_distrib, name="all_sequential_open_distrib"),
|
||||
|
||||
url(r'^{}/all_problem_grade_distribution$'.format(settings.COURSE_ID_PATTERN),
|
||||
class_dashboard.views.all_problem_grade_distribution, name="all_problem_grade_distribution"),
|
||||
|
||||
# Json request data for metrics for particular section
|
||||
url(r'^{}/problem_grade_distribution/(?P<section>\d+)$'.format(settings.COURSE_ID_PATTERN),
|
||||
class_dashboard.views.section_problem_grade_distrib, name="section_problem_grade_distrib"),
|
||||
|
||||
# For listing students that opened a sub-section
|
||||
url(r'^get_students_opened_subsection$',
|
||||
class_dashboard.dashboard_data.get_students_opened_subsection, name="get_students_opened_subsection"),
|
||||
|
||||
# For listing of students' grade per problem
|
||||
url(r'^get_students_problem_grades$',
|
||||
class_dashboard.dashboard_data.get_students_problem_grades, name="get_students_problem_grades"),
|
||||
|
||||
# For generating metrics data as a csv
|
||||
url(r'^post_metrics_data_csv_url',
|
||||
class_dashboard.dashboard_data.post_metrics_data_csv, name="post_metrics_data_csv"),
|
||||
]
|
||||
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
Handles requests for data, returning a json
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.http import HttpResponse
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from class_dashboard import dashboard_data
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.courses import get_course_overview_with_access
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def has_instructor_access_for_class(user, course_id):
|
||||
"""
|
||||
Returns true if the `user` is an instructor for the course.
|
||||
"""
|
||||
|
||||
course = get_course_overview_with_access(user, 'staff', course_id)
|
||||
return bool(has_access(user, 'staff', course))
|
||||
|
||||
|
||||
def all_sequential_open_distrib(request, course_id):
|
||||
"""
|
||||
Creates a json with the open distribution for all the subsections in the course.
|
||||
|
||||
`request` django request
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Returns the format in dashboard_data.get_d3_sequential_open_distrib
|
||||
"""
|
||||
|
||||
data = {}
|
||||
|
||||
# Only instructor for this particular course can request this information
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
if has_instructor_access_for_class(request.user, course_key):
|
||||
try:
|
||||
data = dashboard_data.get_d3_sequential_open_distrib(course_key)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.error(u'Generating metrics failed with exception: %s', ex)
|
||||
data = {'error': "error"}
|
||||
else:
|
||||
data = {'error': "Access Denied: User does not have access to this course's data"}
|
||||
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
|
||||
|
||||
def all_problem_grade_distribution(request, course_id):
|
||||
"""
|
||||
Creates a json with the grade 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_grade_distrib
|
||||
"""
|
||||
data = {}
|
||||
|
||||
# Only instructor for this particular course can request this information
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
if has_instructor_access_for_class(request.user, course_key):
|
||||
try:
|
||||
data = dashboard_data.get_d3_problem_grade_distrib(course_key)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.error(u'Generating metrics failed with exception: %s', ex)
|
||||
data = {'error': "error"}
|
||||
else:
|
||||
data = {'error': "Access Denied: User does not have access to this course's data"}
|
||||
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
|
||||
|
||||
def section_problem_grade_distrib(request, course_id, section):
|
||||
"""
|
||||
Creates a json with the grade distribution for the problems in the specified section.
|
||||
|
||||
`request` django request
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
`section` The zero-based index of the section for the course
|
||||
|
||||
Returns the format in dashboard_data.get_d3_section_grade_distrib
|
||||
|
||||
If this is requested multiple times quickly for the same course, it is better to call all_problem_grade_distribution
|
||||
and pick out the sections of interest.
|
||||
"""
|
||||
data = {}
|
||||
|
||||
# Only instructor for this particular course can request this information
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
if has_instructor_access_for_class(request.user, course_key):
|
||||
try:
|
||||
data = dashboard_data.get_d3_section_grade_distrib(course_key, section)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.error(u'Generating metrics failed with exception: %s', ex)
|
||||
data = {'error': "error"}
|
||||
else:
|
||||
data = {'error': "Access Denied: User does not have access to this course's data"}
|
||||
|
||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||
@@ -30,7 +30,6 @@ from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from bulk_email.api import is_bulk_email_feature_enabled
|
||||
from class_dashboard.dashboard_data import get_array_section_has_problem, get_section_display_name
|
||||
from course_modes.models import CourseMode, CourseModesArchive
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.certificates import api as certs_api
|
||||
@@ -174,10 +173,6 @@ def instructor_dashboard_2(request, course_id):
|
||||
if is_bulk_email_feature_enabled(course_key):
|
||||
sections.append(_section_send_email(course, access))
|
||||
|
||||
# Gate access to Metrics tab by featue flag and staff authorization
|
||||
if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
|
||||
sections.append(_section_metrics(course, access))
|
||||
|
||||
# Gate access to Ecommerce tab
|
||||
if course_mode_has_price and (access['finance_admin'] or access['sales_admin']):
|
||||
sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label, reports_enabled))
|
||||
@@ -798,23 +793,6 @@ def _section_analytics(course, access):
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_metrics(course, access):
|
||||
"""Provide data for the corresponding dashboard section """
|
||||
course_key = course.id
|
||||
section_data = {
|
||||
'section_key': 'metrics',
|
||||
'section_display_name': _('Metrics'),
|
||||
'access': access,
|
||||
'course_id': six.text_type(course_key),
|
||||
'sub_section_display_name': get_section_display_name(course_key),
|
||||
'section_has_problem': get_array_section_has_problem(course_key),
|
||||
'get_students_opened_subsection_url': reverse('get_students_opened_subsection'),
|
||||
'get_students_problem_grades_url': reverse('get_students_problem_grades'),
|
||||
'post_metrics_data_csv_url': reverse('post_metrics_data_csv'),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_open_response_assessment(request, course, openassessment_blocks, access):
|
||||
"""Provide data for the corresponding dashboard section """
|
||||
course_key = course.id
|
||||
|
||||
@@ -2708,11 +2708,6 @@ VERIFICATION_EXPIRY_EMAIL = {
|
||||
|
||||
DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH = "verify_student_disable_account_activation_requirement"
|
||||
|
||||
### This enables the Metrics tab for the Instructor dashboard ###########
|
||||
FEATURES['CLASS_DASHBOARD'] = False
|
||||
if FEATURES.get('CLASS_DASHBOARD'):
|
||||
INSTALLED_APPS.append('class_dashboard')
|
||||
|
||||
################ Enable credit eligibility feature ####################
|
||||
ENABLE_CREDIT_ELIGIBILITY = True
|
||||
FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, allSubsectionTooltipArr, allProblemTooltipArr, **kwargs"/>
|
||||
<%!
|
||||
import json
|
||||
from django.urls import reverse
|
||||
from six import text_type
|
||||
%>
|
||||
|
||||
$(function () {
|
||||
|
||||
d3.json("${reverse('all_sequential_open_distrib', kwargs=dict(course_id=text_type(course_id)))}", function(error, json) {
|
||||
var section, paramOpened, barGraphOpened, error;
|
||||
var i, curr_id;
|
||||
var errorMessage = gettext('Unable to retrieve data, please try again later.');
|
||||
|
||||
error = json.error;
|
||||
if (error) {
|
||||
$('.metrics-left .loading').text(errorMessage);
|
||||
return
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (section in json) {
|
||||
curr_id = "#${id_opened_prefix}"+i;
|
||||
paramOpened = {
|
||||
data: json[section].data,
|
||||
width: $(curr_id).width(),
|
||||
height: $(curr_id).height()-25, // Account for header
|
||||
tag: "opened"+i,
|
||||
bVerticalXAxisLabel : true,
|
||||
bLegend : false,
|
||||
margin: {left:0},
|
||||
};
|
||||
|
||||
// Construct array of tooltips for all sections for the "Download Subsection Data" button.
|
||||
var sectionTooltipArr = new Array();
|
||||
paramOpened.data.forEach( function(element, index, array) {
|
||||
sectionTooltipArr[index] = element.stackData[0].tooltip;
|
||||
});
|
||||
allSubsectionTooltipArr[i] = sectionTooltipArr;
|
||||
|
||||
barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"),
|
||||
d3.select("#${id_tooltip_prefix}"+i));
|
||||
barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
|
||||
|
||||
if (paramOpened.data.length > 0) {
|
||||
barGraphOpened.drawGraph();
|
||||
|
||||
$('svg').siblings('.loading').remove();
|
||||
} else {
|
||||
$('svg').siblings('.loading').text(errorMessage);
|
||||
}
|
||||
|
||||
i+=1;
|
||||
}
|
||||
});
|
||||
|
||||
d3.json("${reverse('all_problem_grade_distribution', kwargs=dict(course_id=text_type(course_id)))}", function(error, json) {
|
||||
var section, paramGrade, barGraphGrade, error;
|
||||
var i, curr_id;
|
||||
var errorMessage = gettext('Unable to retrieve data, please try again later.');
|
||||
|
||||
error = json.error;
|
||||
if (error) {
|
||||
$('.metrics-right .loading').text(errorMessage);
|
||||
return
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (section in json) {
|
||||
curr_id = "#${id_grade_prefix}"+i;
|
||||
paramGrade = {
|
||||
data: json[section].data,
|
||||
width: $(curr_id).width(),
|
||||
height: $(curr_id).height()-25, // Account for header
|
||||
tag: "grade"+i,
|
||||
bVerticalXAxisLabel : true,
|
||||
};
|
||||
|
||||
// Construct array of tooltips for all sections for the "Download Problem Data" button.
|
||||
var sectionTooltipArr = new Array();
|
||||
paramGrade.data.forEach( function(element, index, array) {
|
||||
var stackDataArr = new Array();
|
||||
for (var j = 0; j < element.stackData.length; j++) {
|
||||
stackDataArr[j] = element.stackData[j].tooltip
|
||||
}
|
||||
sectionTooltipArr[index] = stackDataArr;
|
||||
});
|
||||
allProblemTooltipArr[i] = sectionTooltipArr;
|
||||
|
||||
barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"),
|
||||
d3.select("#${id_tooltip_prefix}"+i));
|
||||
barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]);
|
||||
barGraphGrade.legend.width += 2;
|
||||
|
||||
if ( paramGrade.data.length > 0 ) {
|
||||
barGraphGrade.drawGraph();
|
||||
|
||||
$('svg').siblings('.loading').remove();
|
||||
} else {
|
||||
$('svg').siblings('.loading').text(errorMessage);
|
||||
}
|
||||
|
||||
i+=1;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,445 +0,0 @@
|
||||
/*
|
||||
There are three parameters:
|
||||
(1) Parameter is of type object. Inside can include (* marks required):
|
||||
data* - Array of objects with key, value pairs that represent a single stack of bars:
|
||||
xValue - Corresponding value for the x-axis
|
||||
stackData - Array of objects with key, value pairs that represent a bar:
|
||||
color - Defines what "color" the bar will map to
|
||||
value - Maps to the height of the bar, along the y-axis
|
||||
tooltip - (Optional) Text to display on mouse hover
|
||||
|
||||
height - Height of the SVG the graph will be displayed in (default: 500)
|
||||
|
||||
width - Width of the SVG the graph will be displayed in (default: 500)
|
||||
|
||||
margin - Object with key, value pairs for the graph's margins within the SVG (default for all: 10)
|
||||
top - Top margin
|
||||
bottom - Bottom margin
|
||||
right - Right margin
|
||||
left - Left margin
|
||||
|
||||
yRange - Array of two values, representing the min and max respectively. (default: [0, <calculated max>])
|
||||
|
||||
xRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data)
|
||||
|
||||
colorRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data)
|
||||
|
||||
bVerticalXAxisLabel - Boolean whether to make the labels in the x-axis veritcal (default: false)
|
||||
|
||||
bLegend - Boolean if false does not create the graph with a legend (default: true)
|
||||
|
||||
(2) Parameter is a d3 pointer to the SVG the graph will draw itself in.
|
||||
|
||||
(3) Parameter is a d3 pointer to a div that will be used for the graph's tooltip.
|
||||
|
||||
****Does not actually draw graph.**** Returns an object that includes a function
|
||||
drawGraph, for when ready to draw graph. Reason for this is, because of all
|
||||
the defaults, some changes may be needed before drawing the graph
|
||||
|
||||
returns an object with the following:
|
||||
state - All information that can be put in parameters and adding:
|
||||
margin.axisX - margin to accomodate the x-axis
|
||||
margin.axisY - margin to acommodate the y-axis
|
||||
|
||||
drawGraph - function to call when ready to draw graph
|
||||
|
||||
scale - Object containing three d3 scales
|
||||
x - d3 scale for the x-axis
|
||||
y - d3 scale for the y-axis
|
||||
stackColor - d3 scale for the stack color
|
||||
|
||||
axis - Object containg the graph's two d3 axis
|
||||
x - d3 axis for the x-axis
|
||||
y - d3 axis for the y-axis
|
||||
|
||||
svg - d3 pointer to the svg holding the graph
|
||||
|
||||
svgGroup - object holding the svg groups
|
||||
main - svg group holding all other groups
|
||||
xAxis - svg group holding the x-axis
|
||||
yAxis - svg group holding the x-axis
|
||||
bars - svg groups holding the bars
|
||||
|
||||
yAxisLabel - d3 pointer to the text component that holds the y axis label
|
||||
|
||||
divTooltip - d3 pointer to the div that is used as the tooltip for the graph
|
||||
|
||||
rects - d3 collection of the rects used in the bars
|
||||
|
||||
legend - object containing information for the legend
|
||||
height - height of the legend
|
||||
width - width of the legend (if change, need to update state.margin.axisY also)
|
||||
range - array of values that appears in the legend
|
||||
barHeight - height of a bar in the legend, based on height and length of range
|
||||
*/
|
||||
|
||||
edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
|
||||
var graph = {
|
||||
svg : svg,
|
||||
state : {
|
||||
data : undefined,
|
||||
height : 500,
|
||||
width : 500,
|
||||
margin: {top: 10, bottom: 10, right: 10, left: 10},
|
||||
yRange: [0],
|
||||
xRange : undefined,
|
||||
colorRange : undefined,
|
||||
tag : "",
|
||||
bVerticalXAxisLabel : false,
|
||||
bLegend : true,
|
||||
},
|
||||
divTooltip : divTooltip,
|
||||
};
|
||||
|
||||
var state = graph.state;
|
||||
|
||||
// Handle parameters
|
||||
state.data = parameters.data;
|
||||
|
||||
if (parameters.margin != undefined) {
|
||||
for (var key in state.margin) {
|
||||
if ((state.margin.hasOwnProperty(key) &&
|
||||
(parameters.margin[key] != undefined))) {
|
||||
state.margin[key] = parameters.margin[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in state) {
|
||||
if ((key != "data") && (key != "margin")) {
|
||||
if (state.hasOwnProperty(key) && (parameters[key] != undefined)) {
|
||||
state[key] = parameters[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.tag != "")
|
||||
state.tag = state.tag+"-";
|
||||
|
||||
if ((state.xRange == undefined) || (state.yRange.length < 2 ||
|
||||
state.colorRange == undefined)) {
|
||||
var aryXRange = [];
|
||||
var bXIsOrdinal = false;
|
||||
var maxYRange = 0;
|
||||
var aryColorRange = [];
|
||||
var bColorIsOrdinal = false;
|
||||
|
||||
for (var stackKey in state.data) {
|
||||
var stack = state.data[stackKey];
|
||||
aryXRange.push(stack.xValue);
|
||||
if (isNaN(stack.xValue))
|
||||
bXIsOrdinal = true;
|
||||
|
||||
var valueTotal = 0;
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
valueTotal += bar.value;
|
||||
|
||||
if (isNaN(bar.color))
|
||||
bColorIsOrdinal = true;
|
||||
|
||||
if (aryColorRange.indexOf(bar.color) < 0)
|
||||
aryColorRange.push(bar.color);
|
||||
}
|
||||
if (maxYRange < valueTotal)
|
||||
maxYRange = valueTotal;
|
||||
}
|
||||
|
||||
if (state.xRange == undefined){
|
||||
if (bXIsOrdinal)
|
||||
state.xRange = aryXRange;
|
||||
else
|
||||
state.xRange = [
|
||||
Math.min.apply(null,aryXRange),
|
||||
Math.max.apply(null,aryXRange)
|
||||
];
|
||||
}
|
||||
|
||||
if (state.yRange.length < 2)
|
||||
state.yRange[1] = maxYRange;
|
||||
|
||||
if (state.colorRange == undefined){
|
||||
if (bColorIsOrdinal)
|
||||
state.colorRange = aryColorRange;
|
||||
else
|
||||
state.colorRange = [
|
||||
Math.min.apply(null,aryColorRange),
|
||||
Math.max.apply(null,aryColorRange)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Find needed spacing for axes
|
||||
var tmpEl = graph.svg.append("text").text(state.yRange[1]+"1234")
|
||||
.attr("id",state.tag+"stacked-bar-graph-long-str");
|
||||
state.margin.axisY = document.getElementById(state.tag+"stacked-bar-graph-long-str")
|
||||
.getComputedTextLength()+state.margin.left;
|
||||
|
||||
var longestXAxisStr = "";
|
||||
if (isNaN(state.xRange[0])) {
|
||||
for (var i in state.xRange) {
|
||||
if (longestXAxisStr.length < state.xRange[i].length)
|
||||
longestXAxisStr = state.xRange[i]+"1234";
|
||||
}
|
||||
} else {
|
||||
longestXAxisStr = state.xRange[1]+"1234";
|
||||
}
|
||||
|
||||
tmpEl.text(longestXAxisStr);
|
||||
if (state.bVerticalXAxisLabel) {
|
||||
state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str")
|
||||
.getComputedTextLength()+state.margin.bottom;
|
||||
} else {
|
||||
state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str")
|
||||
.clientHeight+state.margin.bottom;
|
||||
}
|
||||
|
||||
tmpEl.remove();
|
||||
|
||||
// Add y0 and y1 of the y-axis based on the count and order of the colorRange.
|
||||
// First, case if color is a number range
|
||||
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
|
||||
!(isNaN(state.colorRange[1]))) {
|
||||
for (var stackKey in state.data) {
|
||||
var stack = state.data[stackKey];
|
||||
stack.stackData.sort(function(a,b) { return a.color - b.color; });
|
||||
|
||||
var currTotal = 0;
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
bar.y0 = currTotal;
|
||||
currTotal += bar.value;
|
||||
bar.y1 = currTotal;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var stackKey in state.data) {
|
||||
var stack = state.data[stackKey];
|
||||
|
||||
var tmpStackData = [];
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
tmpStackData[state.colorRange.indexOf(bar.color)] = bar;
|
||||
}
|
||||
stack.stackData = tmpStackData;
|
||||
|
||||
var currTotal = 0;
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
bar.y0 = currTotal;
|
||||
currTotal += bar.value;
|
||||
bar.y1 = currTotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add information to create legend
|
||||
if (state.bLegend) {
|
||||
graph.legend = {
|
||||
height : (state.height-state.margin.top-state.margin.axisX),
|
||||
width : 30,
|
||||
range : state.colorRange,
|
||||
};
|
||||
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
|
||||
!(isNaN(state.colorRange[1]))) {
|
||||
graph.legend.range = [];
|
||||
|
||||
var i = 0;
|
||||
var min = state.colorRange[0];
|
||||
var max = state.colorRange[1];
|
||||
while (i <= 10) {
|
||||
graph.legend.range[i] = min+((max-min)/10)*i;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
graph.legend.barHeight = graph.legend.height/graph.legend.range.length;
|
||||
|
||||
// Shifting the axis over to make room
|
||||
graph.state.margin.axisY += graph.legend.width;
|
||||
}
|
||||
|
||||
// Make the scales
|
||||
graph.scale = {
|
||||
x: d3.scale.ordinal()
|
||||
.domain(graph.state.xRange)
|
||||
.rangeRoundBands([
|
||||
(graph.state.margin.axisY),
|
||||
(graph.state.width-graph.state.margin.right)],
|
||||
.3),
|
||||
|
||||
y: d3.scale.linear()
|
||||
.domain(graph.state.yRange) // yRange is the range of the y-axis values
|
||||
.range([
|
||||
(graph.state.height-graph.state.margin.axisX),
|
||||
graph.state.margin.top
|
||||
]),
|
||||
|
||||
stackColor: d3.scale.ordinal()
|
||||
.domain(graph.state.colorRange)
|
||||
.range(["#ffeeee","#ffebeb","#ffd8d8","#ffc4c4","#ffb1b1","#ff9d9d","#ff8989","#ff7676","#ff6262","#ff4e4e","#ff3b3b"])
|
||||
};
|
||||
|
||||
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
|
||||
!(isNaN(state.colorRange[1]))) {
|
||||
graph.scale.stackColor = d3.scale.linear()
|
||||
.domain(state.colorRange)
|
||||
.range(["#e13f29","#17a74d"]);
|
||||
}
|
||||
|
||||
// Setup axes
|
||||
graph.axis = {
|
||||
x: d3.svg.axis()
|
||||
.scale(graph.scale.x),
|
||||
y: d3.svg.axis()
|
||||
.scale(graph.scale.y),
|
||||
}
|
||||
|
||||
graph.axis.x.orient("bottom");
|
||||
graph.axis.y.orient("left");
|
||||
|
||||
// Draw graph function, to call when ready.
|
||||
graph.drawGraph = function() {
|
||||
var graph = this;
|
||||
|
||||
// Steup SVG
|
||||
graph.svg.attr("id", graph.state.tag+"stacked-bar-graph")
|
||||
.attr("class", "stacked-bar-graph")
|
||||
.attr("width", graph.state.width)
|
||||
.attr("height", graph.state.height);
|
||||
graph.svgGroup = {};
|
||||
|
||||
graph.svgGroup.main = graph.svg.append("g");
|
||||
|
||||
// Draw Bars
|
||||
graph.svgGroup.bars = graph.svgGroup.main.selectAll(".stacked-bar")
|
||||
.data(graph.state.data)
|
||||
.enter().append("g")
|
||||
.attr("class", "stacked-bar")
|
||||
.attr("transform", function(d) {
|
||||
return "translate("+graph.scale.x(d.xValue)+",0)";
|
||||
});
|
||||
|
||||
graph.rects = graph.svgGroup.bars.selectAll("rect")
|
||||
.data(function(d) { return d.stackData; })
|
||||
.enter().append("rect")
|
||||
.attr("width", function(d) {
|
||||
return graph.scale.x.rangeBand()
|
||||
})
|
||||
.attr("y", function(d) { return graph.scale.y(d.y1); })
|
||||
.attr("height", function(d) {
|
||||
return graph.scale.y(d.y0) - graph.scale.y(d.y1);
|
||||
})
|
||||
.attr("id", function(d) { return d.module_url })
|
||||
.style("fill", function(d) { return graph.scale.stackColor(d.color); })
|
||||
.style("stroke", "white")
|
||||
.style("stroke-width", "0.5px");
|
||||
|
||||
// Setup tooltip
|
||||
if (graph.divTooltip != undefined) {
|
||||
graph.divTooltip
|
||||
.style("position", "absolute")
|
||||
.style("z-index", "10")
|
||||
.style("visibility", "hidden");
|
||||
}
|
||||
|
||||
graph.rects
|
||||
.on("mouseover", function(d) {
|
||||
var pos = d3.mouse(graph.divTooltip.node().parentNode);
|
||||
var left = pos[0]+10;
|
||||
var top = pos[1]-10;
|
||||
var width = $('#'+graph.divTooltip.attr("id")).width();
|
||||
|
||||
// Construct the tooltip
|
||||
if (d.tooltip['type'] == 'subsection') {
|
||||
stud_str = ngettext('%(num_students)s student opened Subsection', '%(num_students)s students opened Subsection', d.tooltip['num_students']);
|
||||
stud_str = interpolate(stud_str, {'num_students': d.tooltip['num_students']}, true);
|
||||
tooltip_str = stud_str + ' ' + d.tooltip['subsection_num'] + ': ' + d.tooltip['subsection_name'];
|
||||
}else if (d.tooltip['type'] == 'problem') {
|
||||
stud_str = ngettext('%(num_students)s student', '%(num_students)s students', d.tooltip['count_grade']);
|
||||
stud_str = interpolate(stud_str, {'num_students': d.tooltip['count_grade']}, true);
|
||||
q_str = ngettext('%(num_questions)s question', '%(num_questions)s questions', d.tooltip['max_grade']);
|
||||
q_str = interpolate(q_str, {'num_questions': d.tooltip['max_grade']}, true);
|
||||
|
||||
tooltip_str = d.tooltip['label'] + ' ' + d.tooltip['problem_name'] + ' - ' \
|
||||
+ stud_str + ' (' + d.tooltip['student_count_percent'] + '%) (' \
|
||||
+ d.tooltip['percent'] + '%: ' + d.tooltip['grade'] +'/' \
|
||||
+ q_str + ')';
|
||||
}
|
||||
graph.divTooltip.style("visibility", "visible")
|
||||
.text(tooltip_str);
|
||||
|
||||
if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width())
|
||||
left -= (width+30);
|
||||
|
||||
graph.divTooltip.style("top", top+"px")
|
||||
.style("left", left+"px");
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
graph.divTooltip.style("visibility", "hidden")
|
||||
});
|
||||
|
||||
// Add legend
|
||||
if (graph.state.bLegend) {
|
||||
graph.svgGroup.legendG = graph.svgGroup.main.append("g")
|
||||
.attr("class","stacked-bar-graph-legend")
|
||||
.attr("transform","translate("+graph.state.margin.left+","+
|
||||
graph.state.margin.top+")");
|
||||
graph.svgGroup.legendGs = graph.svgGroup.legendG.selectAll(".stacked-bar-graph-legend-g")
|
||||
.data(graph.legend.range)
|
||||
.enter().append("g")
|
||||
.attr("class","stacked-bar-graph-legend-g")
|
||||
.attr("id",function(d,i) { return graph.state.tag+"legend-"+i; })
|
||||
.attr("transform", function(d,i) {
|
||||
return "translate(0,"+
|
||||
(graph.state.height-graph.state.margin.axisX-((i+1)*(graph.legend.barHeight))) + ")";
|
||||
});
|
||||
|
||||
graph.svgGroup.legendGs.append("rect")
|
||||
.attr("class","stacked-bar-graph-legend-rect")
|
||||
.attr("height", graph.legend.barHeight)
|
||||
.attr("width", graph.legend.width)
|
||||
.style("fill", graph.scale.stackColor)
|
||||
.style("stroke", "white");
|
||||
|
||||
graph.svgGroup.legendGs.append("text")
|
||||
.attr("class","axis-label")
|
||||
.attr("transform", function(d) {
|
||||
var str = "translate("+(graph.legend.width/2)+","+
|
||||
(graph.legend.barHeight/2)+")";
|
||||
return str;
|
||||
})
|
||||
.attr("dy", ".35em")
|
||||
.attr("dx", "-1px")
|
||||
.style("text-anchor", "middle")
|
||||
.text(function(d,i) { return d; });
|
||||
}
|
||||
|
||||
|
||||
// Draw Axes
|
||||
graph.svgGroup.xAxis = graph.svgGroup.main.append("g")
|
||||
.attr("class","stacked-bar-graph-axis")
|
||||
.attr("id",graph.state.tag+"x-axis");
|
||||
|
||||
var tmpS = "translate(0,"+(graph.state.height-graph.state.margin.axisX)+")";
|
||||
if (graph.state.bVerticalXAxisLabel) {
|
||||
graph.axis.x.orient("left");
|
||||
tmpS = "rotate(270), translate(-"+(graph.state.height-graph.state.margin.axisX)+",0)";
|
||||
}
|
||||
graph.svgGroup.xAxis.attr("transform", tmpS)
|
||||
.call(graph.axis.x);
|
||||
|
||||
graph.svgGroup.yAxis = graph.svgGroup.main.append("g")
|
||||
.attr("class","stacked-bar-graph-axis")
|
||||
.attr("id",graph.state.tag+"y-axis")
|
||||
.attr("transform","translate("+
|
||||
(graph.state.margin.axisY)+",0)")
|
||||
.call(graph.axis.y);
|
||||
graph.yAxisLabel = graph.svgGroup.yAxis.append("text")
|
||||
.attr("dy","1em")
|
||||
.attr("transform","rotate(-90)")
|
||||
.style("text-anchor","end")
|
||||
.text(gettext("Number of Students"));
|
||||
};
|
||||
|
||||
return graph;
|
||||
};
|
||||
@@ -1,276 +0,0 @@
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template.defaultfilters import escapejs
|
||||
%>
|
||||
|
||||
<%page args="section_data"/>
|
||||
|
||||
<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"/>
|
||||
<div id="graph_reload">
|
||||
<p>${_("Use Reload Graphs to refresh the graphs.")}</p>
|
||||
<p><input type="button" value="${_("Reload Graphs")}"/></p>
|
||||
</div>
|
||||
<div class="metrics-header-container">
|
||||
<div class="metrics-left-header">
|
||||
<h3 class="hd hd-3">${_("Subsection Data")}</h3>
|
||||
<p>${_("Each bar shows the number of students that opened the subsection.")}</p>
|
||||
<p>${_("You can click on any of the bars to list the students that opened the subsection.")}</p>
|
||||
<p>${_("You can also download this data as a CSV file.")}</p>
|
||||
<p><input type="button" id="download_subsection_data" value="${_("Download Subsection Data for all Subsections as a CSV")}" /></p>
|
||||
</div>
|
||||
<div class="metrics-right-header">
|
||||
<h3 class="hd hd-3">${_("Grade Distribution Data")}</h3>
|
||||
<p>${_("Each bar shows the grade distribution for that problem.")}</p>
|
||||
<p>${_("You can click on any of the bars to list the students that attempted the problem, along with the grades they received.")}</p>
|
||||
<p>${_("You can also download this data as a CSV file.")}</p>
|
||||
<p><input type="button" id="download_problem_data" value="${_("Download Problem Data for all Problems as a CSV")}" /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- For each section with data, create the divs for displaying the graphs
|
||||
and the popup window for listing the students
|
||||
-->
|
||||
%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-section metrics-left" id="metric_opened_${i}">
|
||||
</div>
|
||||
<div class="metrics-section metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}>
|
||||
<h3>${_("Grade Distribution per Problem")}</h3>
|
||||
</div>
|
||||
<div class="metrics-overlay">
|
||||
<div class="metrics-overlay-content-wrapper">
|
||||
<div class="metrics-overlay-content">
|
||||
<table>
|
||||
<thead></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<input class="download-csv metrics-student-opened" type="button" name="dump_student_opened" value="${_("Download Student Opened as a CSV")}" data-endpoint="${section_data['get_students_opened_subsection_url']}" data-csv="true">
|
||||
<input class="download-csv metrics-student-grades" type="button" name="dump_student_grades" value="${_("Download Student Grades as a CSV")}" data-endpoint="${section_data['get_students_problem_grades_url']}" data-csv="true">
|
||||
<a class="close-button" href="#"><span class="icon fa fa-remove" aria-hidden="true"></span><span class="sr">${_("Close")}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
<script>
|
||||
$(function () {
|
||||
var firstLoad = true;
|
||||
var allSubsectionTooltipArr = new Array();
|
||||
var allProblemTooltipArr = new Array();
|
||||
|
||||
// Click handler for left bars
|
||||
$('.metrics-container').on("click", '.metrics-left .stacked-bar', function () {
|
||||
var module_id = $('rect', this).attr('id');
|
||||
var metrics_overlay = $(this).closest('.metrics-left').siblings('.metrics-overlay');
|
||||
|
||||
// Set module_id attribute on metrics_overlay
|
||||
metrics_overlay.data("module-id", module_id);
|
||||
|
||||
var header = $(this).closest('.metrics-left').siblings('.metrics-tooltip').text();
|
||||
var overlay_content = '<h3 class="metrics-overlay-title">' + header + '</h3>';
|
||||
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
|
||||
|
||||
$.ajax({
|
||||
url: "${section_data['get_students_opened_subsection_url']}",
|
||||
type: "GET",
|
||||
data: {module_id: module_id},
|
||||
dataType: "json",
|
||||
|
||||
success: function(response) {
|
||||
overlay_content = "<tr class='header'><th>${_('Name')}</th><th>${_('Username')}</th></tr>";
|
||||
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
|
||||
|
||||
$.each(response.results, function(index, value ){
|
||||
overlay_content = '<tr><td>' + _.escape(value['name']) + "</td><td>" + _.escape(value['username']) + '</td></tr>';
|
||||
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
|
||||
});
|
||||
// If student list too long, append message to screen.
|
||||
if (response.max_exceeded) {
|
||||
overlay_content = "<p class='overflow-message'>${_('This is a partial list, to view all students download as a csv.')}</p>";
|
||||
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
|
||||
}
|
||||
}
|
||||
})
|
||||
metrics_overlay.find('.metrics-student-opened').show();
|
||||
metrics_overlay.show();
|
||||
});
|
||||
|
||||
// Click handler for right bars
|
||||
$('.metrics-container').on("click", '.metrics-right .stacked-bar', function () {
|
||||
var module_id = $('rect', this).attr('id');
|
||||
var metrics_overlay = $(this).closest('.metrics-right').siblings('.metrics-overlay');
|
||||
|
||||
//Set module_id attribute on metrics_overlay
|
||||
metrics_overlay.data("module-id", module_id);
|
||||
|
||||
var header = $(this).closest('.metrics-right').siblings('.metrics-tooltip').text();
|
||||
var far_index = header.indexOf(' - ');
|
||||
var title = header.substring(0, far_index);
|
||||
|
||||
var overlay_content = '<h3 class="metrics-overlay-title">' + title + '</h3>';
|
||||
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
|
||||
|
||||
$.ajax({
|
||||
url: "${section_data['get_students_problem_grades_url']}",
|
||||
type: "GET",
|
||||
data: {module_id: module_id},
|
||||
dataType: "json",
|
||||
|
||||
success: function(response) {
|
||||
overlay_content = "<tr class='header'><th>${_('Name')}</th><th>${_('Username')}</th><th>${_('Grade')}</th><th>${_('Percent')}</th></tr>";
|
||||
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
|
||||
|
||||
$.each(response.results, function(index, value ){
|
||||
overlay_content = '<tr><td>' + _.escape(value['name']) + "</td><td>" + _.escape(value['username']) + "</td><td>" + _.escape(value['grade']) + "</td><td>" + _.escape(value['percent']) + '</td></tr>';
|
||||
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
|
||||
});
|
||||
// If student list too long, append message to screen.
|
||||
if (response.max_exceeded) {
|
||||
overlay_content = "<p class='overflow-message'>${_('This is a partial list, to view all students download as a csv.')}</p>";
|
||||
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
|
||||
}
|
||||
},
|
||||
})
|
||||
metrics_overlay.find('.metrics-student-grades').show();
|
||||
metrics_overlay.show();
|
||||
});
|
||||
|
||||
loadGraphs = function() {
|
||||
$('#graph_reload').hide();
|
||||
$('.metrics-header-container').hide();
|
||||
$('.loading').remove();
|
||||
|
||||
|
||||
var nothingText = "${_('There are no problems in this section.')}";
|
||||
var loadingText = "${_('Loading')}";
|
||||
var nothingP = '<p class="nothing">' + nothingText + '</p>';
|
||||
var loading = '<p class="loading"><span class="icon fa fa-spinner fa-spin fa-large" aria-hidden="true"></span>' + loadingText + '</p>';
|
||||
|
||||
// Display spinners or "There are no problems in this section" message
|
||||
$('.metrics-left').each(function() {
|
||||
$(this).append(loading);
|
||||
});
|
||||
$('.metrics-right p.nothing').remove();
|
||||
$('.metrics-right').each(function() {
|
||||
if ($(this).data('section-has-problem') === "False") {
|
||||
$(this).append(nothingP);
|
||||
} else {
|
||||
$(this).append(loading);
|
||||
}
|
||||
});
|
||||
$('.metrics-left svg, .metrics-right svg').remove();
|
||||
|
||||
${all_section_metrics.body("metric_opened_", "metric_grade_", "metric_attempts_", "metric_tooltip_", course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
|
||||
}
|
||||
|
||||
// For downloading subsection and problem data as csv
|
||||
download_csv_data = function(event) {
|
||||
|
||||
var allSectionArr = []
|
||||
var allTooltipArr = []
|
||||
if (event.type == 'subsection') {
|
||||
allTooltipArr = allSubsectionTooltipArr;
|
||||
} else if (event.type == 'problem') {
|
||||
allTooltipArr = allProblemTooltipArr;
|
||||
}
|
||||
allTooltipArr.forEach( function(element, index, array) {
|
||||
|
||||
var metrics_section = 'metrics_section' + '_' + index
|
||||
// Get Section heading which is everything after first ': '
|
||||
var heading = $('#' + metrics_section).children('h2').text();
|
||||
allSectionArr[index] = heading.substr(heading.indexOf(': ') +2)
|
||||
});
|
||||
|
||||
var data = {}
|
||||
data['sections'] = JSON.stringify(allSectionArr);
|
||||
data['tooltips'] = JSON.stringify(allTooltipArr);
|
||||
data['course_id'] = "${section_data['course_id'] | escapejs}";
|
||||
data['data_type'] = event.type;
|
||||
|
||||
var input_data = document.createElement("input");
|
||||
input_data.name = 'data';
|
||||
input_data.value = JSON.stringify(data);
|
||||
|
||||
var csrf_token_input = document.createElement("input");
|
||||
csrf_token_input.name = 'csrfmiddlewaretoken';
|
||||
csrf_token_input.value = "${ csrf_token }"
|
||||
|
||||
// Send data as a POST so it doesn't create a huge url
|
||||
var form = document.createElement("form");
|
||||
form.action = "${section_data['post_metrics_data_csv_url']}";
|
||||
form.method = 'post'
|
||||
|
||||
form.appendChild(input_data);
|
||||
form.appendChild(csrf_token_input)
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
$('.instructor-nav a').click(function () {
|
||||
if ($(this).data('section') === "metrics" && firstLoad) {
|
||||
loadGraphs();
|
||||
firstLoad = false;
|
||||
$('#graph_reload').show();
|
||||
$('.metrics-header-container').show();
|
||||
}
|
||||
});
|
||||
|
||||
$('#graph_reload').click(function () {
|
||||
loadGraphs();
|
||||
$('#graph_reload').show();
|
||||
$('.metrics-header-container').show();
|
||||
});
|
||||
|
||||
$('#download_subsection_data').click(function() {
|
||||
download_csv_data({'type': 'subsection'});
|
||||
});
|
||||
|
||||
$('#download_problem_data').click(function() {
|
||||
download_csv_data({'type': 'problem'});
|
||||
});
|
||||
|
||||
if (window.location.hash === "#view-metrics") {
|
||||
$('.instructor-nav [data-section="metrics"]').click();
|
||||
$('#graph_reload').hide();
|
||||
$('.metrics-header-container').hide();
|
||||
}
|
||||
|
||||
$(document).ajaxStop(function() {
|
||||
$('#graph_reload').show();
|
||||
$('.metrics-header-container').show();
|
||||
});
|
||||
|
||||
});
|
||||
$('.metrics-overlay .close-button').click(function(event) {
|
||||
event.preventDefault();
|
||||
$('.metrics-overlay-content table thead, .metrics-overlay-content table tbody').empty();
|
||||
$('.metrics-overlay-content-wrapper h3').remove();
|
||||
$('.metrics-overlay-content-wrapper p').remove();
|
||||
$(this).closest(".metrics-overlay").hide();
|
||||
$('.metrics-overlay .download-csv').hide();
|
||||
});
|
||||
$('.metrics-overlay .download-csv').click(function(event) {
|
||||
|
||||
var module_id = $(this).closest('.metrics-overlay').data("module-id");
|
||||
var tooltip = $(this).closest('.metrics-container').children('.metrics-tooltip').text();
|
||||
var attributes = '?module_id=' + module_id + '&csv=true' + '&tooltip=' + tooltip;
|
||||
var url = $(this).data("endpoint");
|
||||
url += attributes;
|
||||
|
||||
return location.href = url;
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
%endif
|
||||
@@ -771,11 +771,6 @@ if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'):
|
||||
),
|
||||
]
|
||||
|
||||
if settings.FEATURES.get('CLASS_DASHBOARD'):
|
||||
urlpatterns += [
|
||||
url(r'^class_dashboard/', include('class_dashboard.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
# Jasmine and admin
|
||||
urlpatterns += [
|
||||
|
||||
Reference in New Issue
Block a user