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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
30
lms/djangoapps/class_dashboard/urls.py
Normal file
30
lms/djangoapps/class_dashboard/urls.py
Normal 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"),
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user