MIT: CCX. Implement coach interactions with student grades
Story #4: Coaches sees grades. Story #9: Coach downloads grades.
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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>
|
||||
|
||||
11
lms/templates/pocs/student_admin.html
Normal file
11
lms/templates/pocs/student_admin.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<%! 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"),
|
||||
|
||||
Reference in New Issue
Block a user