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
This commit is contained in:
@@ -28,7 +28,11 @@ from ..models import (
|
||||
PocMembership,
|
||||
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 (
|
||||
PocFactory,
|
||||
PocMembershipFactory,
|
||||
@@ -73,13 +77,18 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
for _ in xrange(2)]
|
||||
sequentials = flatten([
|
||||
[ItemFactory.create(parent=chapter) for _ in xrange(2)]
|
||||
for chapter in chapters])
|
||||
for chapter in chapters]
|
||||
)
|
||||
verticals = flatten([
|
||||
[ItemFactory.create(due=due, parent=sequential) for _ in xrange(2)]
|
||||
for sequential in sequentials])
|
||||
[ItemFactory.create(
|
||||
due=due, parent=sequential, graded=True, format='Homework')
|
||||
for _ in xrange(2)]
|
||||
for sequential in sequentials]
|
||||
)
|
||||
blocks = flatten([
|
||||
[ItemFactory.create(parent=vertical) for _ in xrange(2)]
|
||||
for vertical in verticals])
|
||||
for vertical in verticals]
|
||||
)
|
||||
|
||||
def make_coach(self):
|
||||
role = CoursePocCoachRole(self.course.id)
|
||||
@@ -140,7 +149,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertTrue(re.search('id="poc-schedule"', response.content))
|
||||
|
||||
@patch('pocs.views.render_to_response', intercept_renderer)
|
||||
@patch('pocs.views.today')
|
||||
@patch('pocs.views.TODAY')
|
||||
def test_edit_schedule(self, today):
|
||||
"""
|
||||
Get POC schedule, modify it, save it.
|
||||
@@ -166,14 +175,23 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'save_poc',
|
||||
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]['children'][0]['due'] = u'2014-12-25 00:00' # what a jerk!
|
||||
response = self.client.post(
|
||||
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]['start'], u'2014-11-20 00:00')
|
||||
self.assertEqual(
|
||||
@@ -186,6 +204,18 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
course_start = get_override_for_poc(poc, self.course, 'start')
|
||||
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):
|
||||
"""enroll a list of students who are members of the class
|
||||
"""
|
||||
@@ -498,12 +528,15 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'progress',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()}
|
||||
)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
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)
|
||||
|
||||
with poc_context(self.poc):
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
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):
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
from copy import deepcopy
|
||||
from cStringIO import StringIO
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -77,6 +78,8 @@ def dashboard(request, course):
|
||||
"""
|
||||
poc = get_poc_for_coach(course, request.user)
|
||||
schedule = get_poc_schedule(course, poc)
|
||||
grading_policy = get_override_for_poc(poc, course, 'grading_policy',
|
||||
course.grading_policy)
|
||||
context = {
|
||||
'course': course,
|
||||
'poc': poc,
|
||||
@@ -87,6 +90,9 @@ def dashboard(request, course):
|
||||
kwargs={'course_id': course.id}),
|
||||
'grades_csv_url': reverse('poc_grades_csv',
|
||||
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:
|
||||
context['create_poc_url'] = reverse(
|
||||
@@ -135,7 +141,7 @@ def save_poc(request, course):
|
||||
"""
|
||||
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
|
||||
`visible_to_staff_only`, `start` and `due` fields for units in the
|
||||
@@ -161,18 +167,55 @@ def save_poc(request, course):
|
||||
else:
|
||||
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)
|
||||
if children:
|
||||
override_fields(block, children, earliest)
|
||||
override_fields(block, children, graded, earliest)
|
||||
return earliest
|
||||
|
||||
earliest = override_fields(course, json.loads(request.body))
|
||||
graded = {}
|
||||
earliest = override_fields(course, json.loads(request.body), graded)
|
||||
if 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(
|
||||
json.dumps(get_poc_schedule(course, poc)),
|
||||
content_type='application/json')
|
||||
json.dumps({
|
||||
'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):
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<li class="nav-item">
|
||||
<a href="#" data-section="student_admin">${_("Student Admin")}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" data-section="grading_policy">${_("Grading Policy")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<section id="membership" class="idash-section">
|
||||
<%include file="enrollment.html" args="" />
|
||||
@@ -52,6 +55,9 @@
|
||||
<section id="student_admin" class="idash-section">
|
||||
<%include file="student_admin.html" args="" />
|
||||
</section>
|
||||
<section id="grading_policy" class="idash-section">
|
||||
<%include file="grading_policy.html" args="" />
|
||||
</section>
|
||||
%endif
|
||||
|
||||
</section>
|
||||
|
||||
10
lms/templates/pocs/grading_policy.html
Normal file
10
lms/templates/pocs/grading_policy.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<%! 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 () {
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(this.schedule),
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
self.schedule = data;
|
||||
self.schedule = data.schedule;
|
||||
self.dirty = false;
|
||||
self.render();
|
||||
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) {
|
||||
console.log(jqXHR.responseText);
|
||||
|
||||
@@ -357,6 +357,8 @@ if settings.COURSEWARE_ENABLED:
|
||||
'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/{}/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),
|
||||
'instructor.views.instructor_dashboard.set_course_mode_price', name="set_course_mode_price"),
|
||||
url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN),
|
||||
|
||||
Reference in New Issue
Block a user