diff --git a/lms/djangoapps/pocs/tests/test_views.py b/lms/djangoapps/pocs/tests/test_views.py index 0f5e3e0ba2..be5c830eb3 100644 --- a/lms/djangoapps/pocs/tests/test_views.py +++ b/lms/djangoapps/pocs/tests/test_views.py @@ -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): diff --git a/lms/djangoapps/pocs/views.py b/lms/djangoapps/pocs/views.py index 89c53f4e2f..30eba263a9 100644 --- a/lms/djangoapps/pocs/views.py +++ b/lms/djangoapps/pocs/views.py @@ -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') + diff --git a/lms/templates/pocs/coach_dashboard.html b/lms/templates/pocs/coach_dashboard.html index 3ad742619d..547fb20280 100644 --- a/lms/templates/pocs/coach_dashboard.html +++ b/lms/templates/pocs/coach_dashboard.html @@ -39,6 +39,9 @@ +
<%include file="enrollment.html" args="" /> @@ -46,6 +49,9 @@
<%include file="schedule.html" args="" />
+
+ <%include file="student_admin.html" args="" /> +
%endif
diff --git a/lms/templates/pocs/student_admin.html b/lms/templates/pocs/student_admin.html new file mode 100644 index 0000000000..64663f319d --- /dev/null +++ b/lms/templates/pocs/student_admin.html @@ -0,0 +1,11 @@ +<%! from django.utils.translation import ugettext as _ %> + +
+

${_('Student Grades')}

+

+ ${_('View gradebook')} +

+

+ ${_('Download student grades')} +

+
diff --git a/lms/urls.py b/lms/urls.py index cc824c638c..8081156229 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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"),