Merge remote-tracking branch 'edx/master' into opaque-keys-merge-master

Conflicts:
	cms/djangoapps/contentstore/tests/utils.py
	cms/djangoapps/contentstore/views/import_export.py
	cms/djangoapps/contentstore/views/tests/test_import_export.py
	common/djangoapps/student/views.py
	lms/djangoapps/class_dashboard/dashboard_data.py
	lms/djangoapps/instructor/views/instructor_dashboard.py
	lms/static/js/staff_debug_actions.js
	lms/templates/notes.html
	lms/templates/staff_problem_info.html
This commit is contained in:
Calen Pennington
2014-05-16 14:01:20 -04:00
49 changed files with 1226 additions and 463 deletions

View File

@@ -2,6 +2,7 @@
Computes the data to display on the Instructor Dashboard
"""
from util.json_request import JsonResponse
import json
from courseware import models
from django.db.models import Count
@@ -22,9 +23,12 @@ def get_problem_grade_distribution(course_id):
`course_id` the course ID for the course interested in
Output is a dict, where the key is the problem 'module_id' and the value is a dict with:
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
@@ -35,6 +39,7 @@ def get_problem_grade_distribution(course_id):
).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:
@@ -54,7 +59,10 @@ def get_problem_grade_distribution(course_id):
'grade_distrib': [(row['grade'], row['count_grade'])]
}
return prob_grade_distrib
# 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):
@@ -139,7 +147,7 @@ def get_d3_problem_grade_distrib(course_id):
'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem
"""
prob_grade_distrib = get_problem_grade_distribution(course_id)
prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id)
d3_data = []
# Retrieve course object down to problems
@@ -181,19 +189,24 @@ def get_d3_problem_grade_distrib(course_id):
for (grade, count_grade) in problem_info['grade_distrib']:
percent = 0.0
if max_grade > 0:
percent = (grade * 100.0) / max_grade
percent = round((grade * 100.0) / max_grade, 1)
# Construct tooltip for problem in grade distibution view
tooltip = _("{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format(
label=label,
problem_name=problem_name,
count_grade=count_grade,
students=_("students"),
percent=percent,
grade=grade,
max_grade=max_grade,
questions=_("questions"),
)
# Compute percent of students with this grade
student_count_percent = 0
if total_student_count.get(child.location.url(), 0) > 0:
student_count_percent = count_grade * 100 / total_student_count[child.location.url()]
# 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({
@@ -249,11 +262,14 @@ def get_d3_sequential_open_distrib(course_id):
num_students = sequential_open_distrib[subsection.location]
stack_data = []
tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format(
num_students=num_students,
subsection_num=c_subsection,
subsection_name=subsection_name,
)
# 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,
@@ -332,19 +348,18 @@ def get_d3_section_grade_distrib(course_id, section):
for (grade, count_grade) in grade_distrib[problem]['grade_distrib']:
percent = 0.0
if max_grade > 0:
percent = (grade * 100.0) / max_grade
percent = round((grade * 100.0) / max_grade, 1)
# Construct tooltip for problem in grade distibution view
tooltip = _("{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format(
problem_info_x=problem_info[problem]['x_value'],
count_grade=count_grade,
students=_("students"),
percent=percent,
problem_info_n=problem_info[problem]['display_name'],
grade=grade,
max_grade=max_grade,
questions=_("questions"),
)
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,
@@ -418,7 +433,12 @@ def get_students_opened_subsection(request, csv=False):
If 'csv' is True, returns a header array, and an array of arrays in the format:
student names, usernames for CSV download.
"""
<<<<<<< HEAD
module_state_key = Location.from_deprecated_string(request.GET.get('module_id'))
=======
module_id = request.GET.get('module_id')
>>>>>>> edx/master
csv = request.GET.get('csv')
# Query for "opened a subsection" students
@@ -450,9 +470,11 @@ def get_students_opened_subsection(request, csv=False):
return JsonResponse(response_payload)
else:
tooltip = request.GET.get('tooltip')
filename = sanitize_filename(tooltip[tooltip.index('S'):])
header = ['Name', 'Username']
# Subsection name is everything after 3rd space in tooltip
filename = sanitize_filename(' '.join(tooltip.split(' ')[3:]))
header = [_("Name").encode('utf-8'), _("Username").encode('utf-8')]
for student in students:
results.append([student['student__profile__name'], student['student__username']])
@@ -510,7 +532,7 @@ def get_students_problem_grades(request, csv=False):
tooltip = request.GET.get('tooltip')
filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')])
header = ['Name', 'Username', 'Grade', 'Percent']
header = [_("Name").encode('utf-8'), _("Username").encode('utf-8'), _("Grade").encode('utf-8'), _("Percent").encode('utf-8')]
for student in students:
percent = 0
@@ -522,11 +544,60 @@ def get_students_problem_grades(request, csv=False):
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").encode('utf-8'), _("Subsection").encode('utf-8'), _("Opened by this number of students").encode('utf-8')]
filename = sanitize_filename(_('subsections') + '_' + course_id)
elif data_type == 'problem':
header = [_("Section").encode('utf-8'), _("Problem").encode('utf-8'), _("Name").encode('utf-8'), _("Count of Students").encode('utf-8'), _("% of Students").encode('utf-8'), _("Score").encode('utf-8')]
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('ascii')
filename = filename.encode('utf-8')
filename = filename[0:25] + '.csv'
return filename

View File

@@ -94,12 +94,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_problem_grade_distribution(self):
prob_grade_distrib = get_problem_grade_distribution(self.course.id)
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.assertEquals(1, max_grade)
for val in total_student_count.values():
self.assertEquals(USER_COUNT, val)
def test_get_sequential_open_distibution(self):
sequential_open_distrib = get_sequential_open_distrib(self.course.id)
@@ -242,6 +245,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
# Check response contains 1 line for each user +1 for the header
self.assertEquals(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': 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.assertEquals(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': 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.assertEquals(4, len(response.content.splitlines()))
def test_get_section_display_name(self):
section_display_name = get_section_display_name(self.course.id)

View File

@@ -0,0 +1,30 @@
"""
Class Dashboard API endpoint urls.
"""
from django.conf.urls import patterns, url
urlpatterns = patterns('', # nopep8
# Json request data for metrics for entire course
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$',
'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
'class_dashboard.views.all_problem_grade_distribution', name="all_problem_grade_distribution"),
# Json request data for metrics for particular section
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P<section>\d+)$',
'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"),
)

View File

@@ -255,10 +255,17 @@ def _section_metrics(course_key, access):
'section_key': 'metrics',
'section_display_name': ('Metrics'),
'access': access,
<<<<<<< HEAD
'sub_section_display_name': get_section_display_name(course_key),
'section_has_problem': get_array_section_has_problem(course_key),
=======
'course_id': course_id,
'sub_section_display_name': get_section_display_name(course_id),
'section_has_problem': get_array_section_has_problem(course_id),
>>>>>>> edx/master
'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

View File

@@ -4,7 +4,6 @@ from edxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access
from notes.models import Note
from notes.utils import notes_enabled_for_course
from xmodule.annotator_token import retrieve_token
@login_required
@@ -23,8 +22,7 @@ def notes(request, course_id):
'course': course,
'notes': notes,
'student': student,
'storage': storage,
'token': retrieve_token(student.email, course.annotation_token_secret),
'storage': storage
}
return render_to_response('notes.html', context)