Commit eecefec8 by Chris Rossi Committed by cewing

MIT: CCX. Implement coach customization of grading policy for a CCX

Story #35 As a coach I can see and edit the json for the grading policy.

Story #34 Recalculate grading policy

Repair the broken test for grading by providing an explicit POC context for the request to run in
parent 2c586ddb
...@@ -28,7 +28,11 @@ from ..models import ( ...@@ -28,7 +28,11 @@ from ..models import (
PocMembership, PocMembership,
PocFutureMembership, PocFutureMembership,
) )
from ..overrides import get_override_for_poc, override_field_for_poc from ..overrides import (
get_override_for_poc,
override_field_for_poc,
poc_context,
)
from .factories import ( from .factories import (
PocFactory, PocFactory,
PocMembershipFactory, PocMembershipFactory,
...@@ -73,13 +77,18 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -73,13 +77,18 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
for _ in xrange(2)] for _ in xrange(2)]
sequentials = flatten([ sequentials = flatten([
[ItemFactory.create(parent=chapter) for _ in xrange(2)] [ItemFactory.create(parent=chapter) for _ in xrange(2)]
for chapter in chapters]) for chapter in chapters]
)
verticals = flatten([ verticals = flatten([
[ItemFactory.create(due=due, parent=sequential) for _ in xrange(2)] [ItemFactory.create(
for sequential in sequentials]) due=due, parent=sequential, graded=True, format='Homework')
for _ in xrange(2)]
for sequential in sequentials]
)
blocks = flatten([ blocks = flatten([
[ItemFactory.create(parent=vertical) for _ in xrange(2)] [ItemFactory.create(parent=vertical) for _ in xrange(2)]
for vertical in verticals]) for vertical in verticals]
)
def make_coach(self): def make_coach(self):
role = CoursePocCoachRole(self.course.id) role = CoursePocCoachRole(self.course.id)
...@@ -140,7 +149,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -140,7 +149,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertTrue(re.search('id="poc-schedule"', response.content)) self.assertTrue(re.search('id="poc-schedule"', response.content))
@patch('pocs.views.render_to_response', intercept_renderer) @patch('pocs.views.render_to_response', intercept_renderer)
@patch('pocs.views.today') @patch('pocs.views.TODAY')
def test_edit_schedule(self, today): def test_edit_schedule(self, today):
""" """
Get POC schedule, modify it, save it. Get POC schedule, modify it, save it.
...@@ -166,14 +175,23 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -166,14 +175,23 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
'save_poc', 'save_poc',
kwargs={'course_id': self.course.id.to_deprecated_string()}) kwargs={'course_id': self.course.id.to_deprecated_string()})
schedule[0]['hidden'] = False def unhide(unit):
"""
Recursively unhide a unit and all of its children in the POC
schedule.
"""
unit['hidden'] = False
for child in unit.get('children', ()):
unhide(child)
unhide(schedule[0])
schedule[0]['start'] = u'2014-11-20 00:00' schedule[0]['start'] = u'2014-11-20 00:00'
schedule[0]['children'][0]['due'] = u'2014-12-25 00:00' # what a jerk! schedule[0]['children'][0]['due'] = u'2014-12-25 00:00' # what a jerk!
response = self.client.post( response = self.client.post(
url, json.dumps(schedule), content_type='application/json' url, json.dumps(schedule), content_type='application/json'
) )
schedule = json.loads(response.content) schedule = json.loads(response.content)['schedule']
self.assertEqual(schedule[0]['hidden'], False) self.assertEqual(schedule[0]['hidden'], False)
self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00') self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00')
self.assertEqual( self.assertEqual(
...@@ -186,6 +204,18 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -186,6 +204,18 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
course_start = get_override_for_poc(poc, self.course, 'start') course_start = get_override_for_poc(poc, self.course, 'start')
self.assertEqual(str(course_start)[:-9], u'2014-11-20 00:00') self.assertEqual(str(course_start)[:-9], u'2014-11-20 00:00')
# Make sure grading policy adjusted
policy = get_override_for_poc(poc, self.course, 'grading_policy',
self.course.grading_policy)
self.assertEqual(policy['GRADER'][0]['type'], 'Homework')
self.assertEqual(policy['GRADER'][0]['min_count'], 4)
self.assertEqual(policy['GRADER'][1]['type'], 'Lab')
self.assertEqual(policy['GRADER'][1]['min_count'], 0)
self.assertEqual(policy['GRADER'][2]['type'], 'Midterm Exam')
self.assertEqual(policy['GRADER'][2]['min_count'], 0)
self.assertEqual(policy['GRADER'][3]['type'], 'Final Exam')
self.assertEqual(policy['GRADER'][3]['min_count'], 0)
def test_enroll_member_student(self): def test_enroll_member_student(self):
"""enroll a list of students who are members of the class """enroll a list of students who are members of the class
""" """
...@@ -498,12 +528,15 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -498,12 +528,15 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
'progress', 'progress',
kwargs={'course_id': self.course.id.to_deprecated_string()} kwargs={'course_id': self.course.id.to_deprecated_string()}
) )
response = self.client.get(url)
self.assertEqual(response.status_code, 200) with poc_context(self.poc):
grades = response.mako_context['grade_summary']
self.assertEqual(grades['percent'], 0.5) response = self.client.get(url)
self.assertEqual(grades['grade_breakdown'][0]['percent'], 0.5) self.assertEqual(response.status_code, 200)
self.assertEqual(len(grades['section_breakdown']), 4) grades = response.mako_context['grade_summary']
self.assertEqual(grades['percent'], 0.5)
self.assertEqual(grades['grade_breakdown'][0]['percent'], 0.5)
self.assertEqual(len(grades['section_breakdown']), 4)
def flatten(seq): def flatten(seq):
......
...@@ -8,6 +8,7 @@ import json ...@@ -8,6 +8,7 @@ import json
import logging import logging
import pytz import pytz
from copy import deepcopy
from cStringIO import StringIO from cStringIO import StringIO
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -77,6 +78,8 @@ def dashboard(request, course): ...@@ -77,6 +78,8 @@ def dashboard(request, course):
""" """
poc = get_poc_for_coach(course, request.user) poc = get_poc_for_coach(course, request.user)
schedule = get_poc_schedule(course, poc) schedule = get_poc_schedule(course, poc)
grading_policy = get_override_for_poc(poc, course, 'grading_policy',
course.grading_policy)
context = { context = {
'course': course, 'course': course,
'poc': poc, 'poc': poc,
...@@ -87,6 +90,9 @@ def dashboard(request, course): ...@@ -87,6 +90,9 @@ def dashboard(request, course):
kwargs={'course_id': course.id}), kwargs={'course_id': course.id}),
'grades_csv_url': reverse('poc_grades_csv', 'grades_csv_url': reverse('poc_grades_csv',
kwargs={'course_id': course.id}), kwargs={'course_id': course.id}),
'grading_policy': json.dumps(grading_policy, indent=4),
'grading_policy_url': reverse('poc_set_grading_policy',
kwargs={'course_id': course.id}),
} }
if not poc: if not poc:
context['create_poc_url'] = reverse( context['create_poc_url'] = reverse(
...@@ -135,7 +141,7 @@ def save_poc(request, course): ...@@ -135,7 +141,7 @@ def save_poc(request, course):
""" """
poc = get_poc_for_coach(course, request.user) poc = get_poc_for_coach(course, request.user)
def override_fields(parent, data, earliest=None): def override_fields(parent, data, graded, earliest=None):
""" """
Recursively apply POC schedule data to POC by overriding the Recursively apply POC schedule data to POC by overriding the
`visible_to_staff_only`, `start` and `due` fields for units in the `visible_to_staff_only`, `start` and `due` fields for units in the
...@@ -161,18 +167,55 @@ def save_poc(request, course): ...@@ -161,18 +167,55 @@ def save_poc(request, course):
else: else:
clear_override_for_poc(poc, block, 'due') clear_override_for_poc(poc, block, 'due')
if not unit['hidden'] and block.graded:
graded[block.format] = graded.get(block.format, 0) + 1
children = unit.get('children', None) children = unit.get('children', None)
if children: if children:
override_fields(block, children, earliest) override_fields(block, children, graded, earliest)
return earliest return earliest
earliest = override_fields(course, json.loads(request.body)) graded = {}
earliest = override_fields(course, json.loads(request.body), graded)
if earliest: if earliest:
override_field_for_poc(poc, course, 'start', earliest) override_field_for_poc(poc, course, 'start', earliest)
# Attempt to automatically adjust grading policy
changed = False
policy = get_override_for_poc(
poc, course, 'grading_policy', course.grading_policy
)
policy = deepcopy(policy)
grader = policy['GRADER']
for section in grader:
count = graded.get(section.get('type'), 0)
if count < section['min_count']:
changed = True
section['min_count'] = count
if changed:
override_field_for_poc(poc, course, 'grading_policy', policy)
return HttpResponse( return HttpResponse(
json.dumps(get_poc_schedule(course, poc)), json.dumps({
content_type='application/json') 'schedule': get_poc_schedule(course, poc),
'grading_policy': json.dumps(policy, indent=4)}),
content_type='application/json',
)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@coach_dashboard
def set_grading_policy(request, course):
"""
Set grading policy for the POC.
"""
poc = get_poc_for_coach(course, request.user)
override_field_for_poc(
poc, course, 'grading_policy', json.loads(request.POST['policy']))
url = reverse('poc_coach_dashboard', kwargs={'course_id': course.id})
return redirect(url)
def parse_date(datestring): def parse_date(datestring):
......
...@@ -42,6 +42,9 @@ ...@@ -42,6 +42,9 @@
<li class="nav-item"> <li class="nav-item">
<a href="#" data-section="student_admin">${_("Student Admin")}</a> <a href="#" data-section="student_admin">${_("Student Admin")}</a>
</li> </li>
<li class="nav-item">
<a href="#" data-section="grading_policy">${_("Grading Policy")}</a>
</li>
</ul> </ul>
<section id="membership" class="idash-section"> <section id="membership" class="idash-section">
<%include file="enrollment.html" args="" /> <%include file="enrollment.html" args="" />
...@@ -52,6 +55,9 @@ ...@@ -52,6 +55,9 @@
<section id="student_admin" class="idash-section"> <section id="student_admin" class="idash-section">
<%include file="student_admin.html" args="" /> <%include file="student_admin.html" args="" />
</section> </section>
<section id="grading_policy" class="idash-section">
<%include file="grading_policy.html" args="" />
</section>
%endif %endif
</section> </section>
......
<%! from django.utils.translation import ugettext as _ %>
<h2>${_("Grading Policy")}</h2>
<form action="${grading_policy_url}" method="POST">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
<textarea cols="80" style="height: 500px;"
name="policy" id="grading-policy">${grading_policy}</textarea><br/>
<button type="submit">${_("Save Grading Policy")}</button>
</form>
...@@ -436,10 +436,14 @@ var poc_schedule = (function () { ...@@ -436,10 +436,14 @@ var poc_schedule = (function () {
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(this.schedule), data: JSON.stringify(this.schedule),
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
self.schedule = data; self.schedule = data.schedule;
self.dirty = false; self.dirty = false;
self.render(); self.render();
button.prop('disabled', false).text('${_("Save changes")}'); button.prop('disabled', false).text('${_("Save changes")}');
// Update textarea with grading policy JSON, since grading policy
// may have changed.
$('#grading-policy').text(data.grading_policy);
}, },
error: function(jqXHR, textStatus, error) { error: function(jqXHR, textStatus, error) {
console.log(jqXHR.responseText); console.log(jqXHR.responseText);
......
...@@ -357,6 +357,8 @@ if settings.COURSEWARE_ENABLED: ...@@ -357,6 +357,8 @@ if settings.COURSEWARE_ENABLED:
'pocs.views.poc_gradebook', name='poc_gradebook'), 'pocs.views.poc_gradebook', name='poc_gradebook'),
url(r'^courses/{}/poc_grades.csv$'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/poc_grades.csv$'.format(settings.COURSE_ID_PATTERN),
'pocs.views.poc_grades_csv', name='poc_grades_csv'), 'pocs.views.poc_grades_csv', name='poc_grades_csv'),
url(r'^courses/{}/poc_set_grading_policy$'.format(settings.COURSE_ID_PATTERN),
'pocs.views.set_grading_policy', name='poc_set_grading_policy'),
url(r'^courses/{}/set_course_mode_price$'.format(settings.COURSE_ID_PATTERN), 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"), 'instructor.views.instructor_dashboard.set_course_mode_price', name="set_course_mode_price"),
url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN),
......
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