Commit 630d1ce0 by Chris Rossi Committed by cewing

MIT: CCX. Implement coach interactions with student grades

Story #4: Coaches sees grades.

Story #9: Coach downloads grades.
parent cabb1962
......@@ -4,6 +4,8 @@ import re
import pytz
from mock import patch
from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
from django.core.urlresolvers import reverse
from edxmako.shortcuts import render_to_response
......@@ -11,7 +13,9 @@ from student.roles import CoursePocCoachRole
from student.tests.factories import (
AdminFactory,
CourseEnrollmentFactory,
UserFactory,
)
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import (
CourseFactory,
......@@ -295,6 +299,86 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
).exists()
)
USER_COUNT = 2
class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Tests for Personal Online Courses views.
"""
def setUp(self):
"""
Set up tests
"""
self.course = course = CourseFactory.create()
# Create instructor account
self.coach = coach = AdminFactory.create()
self.client.login(username=coach.username, password="test")
# Create a course outline
self.mooc_start = start = datetime.datetime(
2010, 5, 12, 2, 42, tzinfo=pytz.UTC)
chapter = ItemFactory.create(
start=start, parent=course, category='sequential')
section = ItemFactory.create(
parent=chapter,
category="sequential",
metadata={'graded': True, 'format': 'Homework'}
)
role = CoursePocCoachRole(self.course.id)
role.add_users(coach)
self.poc = poc = PocFactory(course_id=self.course.id, coach=self.coach)
self.users = [UserFactory.create() for _ in xrange(USER_COUNT)]
for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
PocMembershipFactory(poc=poc, student=user, active=True)
for i in xrange(USER_COUNT - 1):
category = "problem"
item = ItemFactory.create(
parent_location=section.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
for j, user in enumerate(self.users):
StudentModuleFactory.create(
grade=1 if i < j else 0,
max_grade=1,
student=user,
course_id=self.course.id,
module_state_key=item.location
)
@patch('pocs.views.render_to_response', intercept_renderer)
def test_gradebook(self):
url = reverse(
'poc_gradebook',
kwargs={'course_id': self.course.id.to_deprecated_string()}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
student_info = response.mako_context['students']
self.assertEqual(len(student_info), USER_COUNT)
self.assertEqual(student_info[0]['grade_summary']['percent'], 0.0)
self.assertEqual(student_info[1]['grade_summary']['percent'], 0.02)
self.assertEqual(
student_info[1]['grade_summary']['grade_breakdown'][0]['percent'],
0.015)
def test_grades_csv(self):
url = reverse(
'poc_grades_csv',
kwargs={'course_id': self.course.id.to_deprecated_string()}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
len(response.content.strip().split('\n')), USER_COUNT + 1)
def flatten(seq):
......
import csv
import datetime
import functools
import json
import logging
import pytz
from cStringIO import StringIO
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden
from django.core.exceptions import ValidationError
......@@ -16,10 +19,12 @@ from django.contrib.auth.models import User
from courseware.courses import get_course_by_id
from courseware.field_overrides import disable_overrides
from courseware.grades import iterate_grades_for
from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.roles import CoursePocCoachRole
from instructor.offline_gradecalc import student_grades
from instructor.views.api import _split_input_list
from instructor.views.tools import get_student_from_identifier
......@@ -68,6 +73,10 @@ def dashboard(request, course):
'schedule': json.dumps(schedule, indent=4),
'save_url': reverse('save_poc', kwargs={'course_id': course.id}),
'poc_members': PocMembership.objects.filter(poc=poc),
'gradebook_url': reverse('poc_gradebook',
kwargs={'course_id': course.id}),
'grades_csv_url': reverse('poc_grades_csv',
kwargs={'course_id': course.id}),
}
if not poc:
context['create_poc_url'] = reverse(
......@@ -242,3 +251,77 @@ def poc_invite(request, course):
pass # maybe log this?
url = reverse('poc_coach_dashboard', kwargs={'course_id': course.id})
return redirect(url)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@coach_dashboard
def poc_gradebook(request, course):
"""
Show the gradebook for this POC.
"""
poc = get_poc_for_coach(course, request.user)
enrolled_students = User.objects.filter(
pocmembership__poc=poc,
pocmembership__active=1
).order_by('username').select_related("profile")
student_info = [
{
'username': student.username,
'id': student.id,
'email': student.email,
'grade_summary': student_grades(student, request, course),
'realname': student.profile.name,
}
for student in enrolled_students
]
return render_to_response('courseware/gradebook.html', {
'students': student_info,
'course': course,
'course_id': course.id,
'staff_access': request.user.is_staff,
'ordered_grades': sorted(
course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@coach_dashboard
def poc_grades_csv(request, course):
poc = get_poc_for_coach(course, request.user)
enrolled_students = User.objects.filter(
pocmembership__poc=poc,
pocmembership__active=1
).order_by('username').select_related("profile")
grades = iterate_grades_for(course.id, enrolled_students)
header = None
rows = []
for student, gradeset, err_msg in grades:
if gradeset:
# We were able to successfully grade this student for this course.
if not header:
# Encode the header row in utf-8 encoding in case there are
# unicode characters
header = [section['label'].encode('utf-8')
for section in gradeset[u'section_breakdown']]
rows.append(["id", "email", "username", "grade"] + header)
percents = {
section['label']: section.get('percent', 0.0)
for section in gradeset[u'section_breakdown']
if 'label' in section
}
row_percents = [percents.get(label, 0.0) for label in header]
rows.append([student.id, student.email, student.username,
gradeset['percent']] + row_percents)
buffer = StringIO()
writer = csv.writer(buffer)
for row in rows:
writer.writerow(row)
return HttpResponse(buffer.getvalue(), content_type='text/csv')
......@@ -39,6 +39,9 @@
<li class="nav-item">
<a href="#" data-section="schedule">${_("Schedule")}</a>
</li>
<li class="nav-item">
<a href="#" data-section="student_admin">${_("Student Admin")}</a>
</li>
</ul>
<section id="membership" class="idash-section">
<%include file="enrollment.html" args="" />
......@@ -46,6 +49,9 @@
<section id="schedule" class="idash-section">
<%include file="schedule.html" args="" />
</section>
<section id="student_admin" class="idash-section">
<%include file="student_admin.html" args="" />
</section>
%endif
</section>
......
<%! from django.utils.translation import ugettext as _ %>
<section>
<h2>${_('Student Grades')}</h2>
<p>
<a href="${gradebook_url}">${_('View gradebook')}</a>
</p>
<p>
<a href="${grades_csv_url}">${_('Download student grades')}</a>
</p>
</section>
......@@ -351,6 +351,10 @@ if settings.COURSEWARE_ENABLED:
'pocs.views.save_poc', name='save_poc'),
url(r'^courses/{}/poc_invite$'.format(settings.COURSE_ID_PATTERN),
'pocs.views.poc_invite', name='poc_invite'),
url(r'^courses/{}/poc_gradebook$'.format(settings.COURSE_ID_PATTERN),
'pocs.views.poc_gradebook', name='poc_gradebook'),
url(r'^courses/{}/poc_grades.csv$'.format(settings.COURSE_ID_PATTERN),
'pocs.views.poc_grades_csv', name='poc_grades_csv'),
url(r'^courses/{}/set_course_mode_price$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.instructor_dashboard.set_course_mode_price', name="set_course_mode_price"),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment