diff --git a/lms/djangoapps/pocs/tests/test_views.py b/lms/djangoapps/pocs/tests/test_views.py index 032a5cc..19ddf44 100644 --- a/lms/djangoapps/pocs/tests/test_views.py +++ b/lms/djangoapps/pocs/tests/test_views.py @@ -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): diff --git a/lms/djangoapps/pocs/views.py b/lms/djangoapps/pocs/views.py index 1bcc939..576102c 100644 --- a/lms/djangoapps/pocs/views.py +++ b/lms/djangoapps/pocs/views.py @@ -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): diff --git a/lms/templates/pocs/coach_dashboard.html b/lms/templates/pocs/coach_dashboard.html index ee8e13d..1fb6e8f 100644 --- a/lms/templates/pocs/coach_dashboard.html +++ b/lms/templates/pocs/coach_dashboard.html @@ -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> diff --git a/lms/templates/pocs/grading_policy.html b/lms/templates/pocs/grading_policy.html new file mode 100644 index 0000000..d641ee0 --- /dev/null +++ b/lms/templates/pocs/grading_policy.html @@ -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> diff --git a/lms/templates/pocs/schedule.html b/lms/templates/pocs/schedule.html index 330070f..94788a7 100644 --- a/lms/templates/pocs/schedule.html +++ b/lms/templates/pocs/schedule.html @@ -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); diff --git a/lms/urls.py b/lms/urls.py index 1fb8a40..76ee70f 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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),