Commit aa1333c3 by Chris Rossi Committed by cewing

MIT: CCX. Hide course blocks not in the CCX from view for coaches and students

parent 6cfc3a02
...@@ -11,7 +11,7 @@ from datetime import datetime ...@@ -11,7 +11,7 @@ from datetime import datetime
import dateutil.parser import dateutil.parser
from lazy import lazy from lazy import lazy
from xmodule.exceptions import UndefinedContext
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
from xmodule.tabs import CourseTabList from xmodule.tabs import CourseTabList
...@@ -1213,21 +1213,34 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -1213,21 +1213,34 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
""" """
try:
module = getattr(self, '_xmodule', None)
if not module:
module = self
except UndefinedContext:
module = self
all_descriptors = [] all_descriptors = []
graded_sections = {} graded_sections = {}
def yield_descriptor_descendents(module_descriptor): def yield_descendents(module):
for child in module_descriptor.get_children(): for child in module.get_children():
yield child yield child
for module_descriptor in yield_descriptor_descendents(child): for module_descriptor in yield_descendents(child):
yield module_descriptor yield module_descriptor
<<<<<<< HEAD
for chapter in self.get_children(): for chapter in self.get_children():
for section in chapter.get_children(): for section in chapter.get_children():
if section.graded: if section.graded:
xmoduledescriptors = list(yield_descriptor_descendents(section)) xmoduledescriptors = list(yield_descriptor_descendents(section))
xmoduledescriptors.append(section) xmoduledescriptors.append(section)
=======
for c in module.get_children():
for s in c.get_children():
if s.graded:
xmoduledescriptors = list(yield_descendents(s))
xmoduledescriptors.append(s)
>>>>>>> Hide course blocks not in the CCX from view for coaches and students
# The xmoduledescriptors included here are only the ones that have scores. # The xmoduledescriptors included here are only the ones that have scores.
section_description = { section_description = {
......
...@@ -246,6 +246,8 @@ def _grade(student, request, course, keep_raw_scores): ...@@ -246,6 +246,8 @@ def _grade(student, request, course, keep_raw_scores):
totaled_scores[section_format] = format_scores totaled_scores[section_format] = format_scores
# Grading policy might be overriden by a POC, need to reset it
course.set_grading_policy(course.grading_policy)
grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES) grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES)
# We round the grade here, to make sure that the grade is an whole percentage and # We round the grade here, to make sure that the grade is an whole percentage and
...@@ -329,6 +331,8 @@ def _progress_summary(student, request, course): ...@@ -329,6 +331,8 @@ def _progress_summary(student, request, course):
# This student must not have access to the course. # This student must not have access to the course.
return None return None
course_module = getattr(course_module, '_x_module', course_module)
submissions_scores = sub_api.get_scores(course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)) submissions_scores = sub_api.get_scores(course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id))
chapters = [] chapters = []
...@@ -479,7 +483,7 @@ def manual_transaction(): ...@@ -479,7 +483,7 @@ def manual_transaction():
transaction.commit() transaction.commit()
def iterate_grades_for(course_id, students): def iterate_grades_for(course_or_id, students):
"""Given a course_id and an iterable of students (User), yield a tuple of: """Given a course_id and an iterable of students (User), yield a tuple of:
(student, gradeset, err_msg) for every student enrolled in the course. (student, gradeset, err_msg) for every student enrolled in the course.
...@@ -497,7 +501,10 @@ def iterate_grades_for(course_id, students): ...@@ -497,7 +501,10 @@ def iterate_grades_for(course_id, students):
make up the final grade. (For display) make up the final grade. (For display)
- raw_scores: contains scores for every graded module - raw_scores: contains scores for every graded module
""" """
course = courses.get_course_by_id(course_id) if isinstance(course_or_id, basestring):
course = courses.get_course_by_id(course_or_id)
else:
course = course_or_id
# We make a fake request because grading code expects to be able to look at # We make a fake request because grading code expects to be able to look at
# the request. We have to attach the correct user to the request before # the request. We have to attach the correct user to the request before
...@@ -505,7 +512,7 @@ def iterate_grades_for(course_id, students): ...@@ -505,7 +512,7 @@ def iterate_grades_for(course_id, students):
request = RequestFactory().get('/') request = RequestFactory().get('/')
for student in students: for student in students:
with dog_stats_api.timer('lms.grades.iterate_grades_for', tags=[u'action:{}'.format(course_id)]): with dog_stats_api.timer('lms.grades.iterate_grades_for', tags=[u'action:{}'.format(course.id)]):
try: try:
request.user = student request.user = student
# Grading calls problem rendering, which calls masquerading, # Grading calls problem rendering, which calls masquerading,
...@@ -522,7 +529,7 @@ def iterate_grades_for(course_id, students): ...@@ -522,7 +529,7 @@ def iterate_grades_for(course_id, students):
'Cannot grade student %s (%s) in course %s because of exception: %s', 'Cannot grade student %s (%s) in course %s because of exception: %s',
student.username, student.username,
student.id, student.id,
course_id, course.id,
exc.message exc.message
) )
yield student, {}, exc.message yield student, {}, exc.message
...@@ -81,7 +81,6 @@ def student_grades(student, request, course, keep_raw_scores=False, use_offline= ...@@ -81,7 +81,6 @@ def student_grades(student, request, course, keep_raw_scores=False, use_offline=
This is the main interface to get grades. It has the same parameters as grades.grade, as well This is the main interface to get grades. It has the same parameters as grades.grade, as well
as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB. as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB.
''' '''
if not use_offline: if not use_offline:
return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores) return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores)
......
...@@ -3,6 +3,9 @@ API related to providing field overrides for individual students. This is used ...@@ -3,6 +3,9 @@ API related to providing field overrides for individual students. This is used
by the individual due dates feature. by the individual due dates feature.
""" """
import json import json
import threading
from contextlib import contextmanager
from courseware.field_overrides import FieldOverrideProvider from courseware.field_overrides import FieldOverrideProvider
...@@ -22,10 +25,41 @@ class PersonalOnlineCoursesOverrideProvider(FieldOverrideProvider): ...@@ -22,10 +25,41 @@ class PersonalOnlineCoursesOverrideProvider(FieldOverrideProvider):
return default return default
class _PocContext(threading.local):
"""
A threading local used to implement the `with_poc` context manager, that
keeps track of the POC currently set as the context.
"""
poc = None
_POC_CONTEXT = _PocContext()
@contextmanager
def poc_context(poc):
"""
A context manager which can be used to explicitly set the POC that is in
play for field overrides. This mechanism overrides the standard mechanism
of looking in the user's session to see if they are enrolled in a POC and
viewing that POC.
"""
prev = _POC_CONTEXT.poc
_POC_CONTEXT.poc = poc
yield
_POC_CONTEXT.poc = prev
def get_current_poc(user): def get_current_poc(user):
""" """
TODO Needs to look in user's session TODO Needs to look in user's session
""" """
# If poc context is explicitly set, that takes precedence over the user's
# session.
poc = _POC_CONTEXT.poc
if poc:
return poc
# Temporary implementation. Final implementation will need to look in # Temporary implementation. Final implementation will need to look in
# user's session so user can switch between (potentially multiple) POC and # user's session so user can switch between (potentially multiple) POC and
# MOOC views. See courseware.courses.get_request_for_thread for idea to # MOOC views. See courseware.courses.get_request_for_thread for idea to
......
...@@ -5,9 +5,11 @@ import pytz ...@@ -5,9 +5,11 @@ import pytz
from mock import patch from mock import patch
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.field_overrides import OverrideFieldData
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.helpers import LoginEnrollmentTestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from student.roles import CoursePocCoachRole from student.roles import CoursePocCoachRole
from student.tests.factories import ( from student.tests.factories import (
...@@ -26,7 +28,7 @@ from ..models import ( ...@@ -26,7 +28,7 @@ from ..models import (
PocMembership, PocMembership,
PocFutureMembership, PocFutureMembership,
) )
from ..overrides import get_override_for_poc from ..overrides import get_override_for_poc, override_field_for_poc
from .factories import ( from .factories import (
PocFactory, PocFactory,
PocMembershipFactory, PocMembershipFactory,
...@@ -369,9 +371,8 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -369,9 +371,8 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
) )
USER_COUNT = 2 @override_settings(FIELD_OVERRIDE_PROVIDERS=(
'pocs.overrides.PersonalOnlineCoursesOverrideProvider',))
class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
Tests for Personal Online Courses views. Tests for Personal Online Courses views.
...@@ -391,39 +392,65 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -391,39 +392,65 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
2010, 5, 12, 2, 42, tzinfo=pytz.UTC) 2010, 5, 12, 2, 42, tzinfo=pytz.UTC)
chapter = ItemFactory.create( chapter = ItemFactory.create(
start=start, parent=course, category='sequential') start=start, parent=course, category='sequential')
section = ItemFactory.create( sections = [
parent=chapter, ItemFactory.create(
category="sequential", parent=chapter,
metadata={'graded': True, 'format': 'Homework'} category="sequential",
) metadata={'graded': True, 'format': 'Homework'})
for _ in xrange(4)]
role = CoursePocCoachRole(self.course.id) role = CoursePocCoachRole(self.course.id)
role.add_users(coach) role.add_users(coach)
self.poc = poc = PocFactory(course_id=self.course.id, coach=self.coach) self.poc = poc = PocFactory(course_id=self.course.id, coach=self.coach)
self.users = [UserFactory.create() for _ in xrange(USER_COUNT)] self.student = student = UserFactory.create()
for user in self.users: CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
CourseEnrollmentFactory.create(user=user, course_id=self.course.id) PocMembershipFactory(poc=poc, student=student, active=True)
PocMembershipFactory(poc=poc, student=user, active=True)
for i, section in enumerate(sections):
for i in xrange(USER_COUNT - 1): for j in xrange(4):
category = "problem" item = ItemFactory.create(
item = ItemFactory.create( parent=section,
parent_location=section.location, category="problem",
category=category, data=StringResponseXMLFactory().build_xml(answer='foo'),
data=StringResponseXMLFactory().build_xml(answer='foo'), metadata={'rerandomize': 'always'}
metadata={'rerandomize': 'always'} )
)
for j, user in enumerate(self.users):
StudentModuleFactory.create( StudentModuleFactory.create(
grade=1 if i < j else 0, grade=1 if i < j else 0,
max_grade=1, max_grade=1,
student=user, student=student,
course_id=self.course.id, course_id=self.course.id,
module_state_key=item.location module_state_key=item.location
) )
# Apparently the test harness doesn't use LmsFieldStorage, and I'm not
# sure if there's a way to poke the test harness to do so. So, we'll
# just inject the override field storage in this brute force manner.
OverrideFieldData.provider_classes = None
for block in iter_blocks(course):
block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access
coach, block._field_data) # pylint: disable=protected-access
block._field_data_cache = {}
visible_children(block)
patch_context = patch('pocs.views.get_course_by_id')
get_course = patch_context.start()
get_course.return_value = course
self.addCleanup(patch_context.stop)
override_field_for_poc(poc, course, 'grading_policy', {
'GRADER': [
{'drop_count': 0,
'min_count': 2,
'short_label': 'HW',
'type': 'Homework',
'weight': 1}
],
'GRADE_CUTOFFS': {'Pass': 0.75},
})
override_field_for_poc(
poc, sections[-1], 'visible_to_staff_only', True)
@patch('pocs.views.render_to_response', intercept_renderer) @patch('pocs.views.render_to_response', intercept_renderer)
def test_gradebook(self): def test_gradebook(self):
...@@ -433,13 +460,13 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -433,13 +460,13 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
student_info = response.mako_context['students'] student_info = response.mako_context['students'][0]
self.assertEqual(len(student_info), USER_COUNT) self.assertEqual(student_info['grade_summary']['percent'], 0.5)
self.assertEqual(student_info[0]['grade_summary']['percent'], 0.0)
self.assertEqual(student_info[1]['grade_summary']['percent'], 0.02)
self.assertEqual( self.assertEqual(
student_info[1]['grade_summary']['grade_breakdown'][0]['percent'], student_info['grade_summary']['grade_breakdown'][0]['percent'],
0.015) 0.5)
self.assertEqual(
len(student_info['grade_summary']['section_breakdown']), 4)
def test_grades_csv(self): def test_grades_csv(self):
url = reverse( url = reverse(
...@@ -448,8 +475,35 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -448,8 +475,35 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase):
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( headers, row = (
len(response.content.strip().split('\n')), USER_COUNT + 1) row.strip().split(',') for row in
response.content.strip().split('\n')
)
data = dict(zip(headers, row))
self.assertEqual(data['HW 01'], '0.75')
self.assertEqual(data['HW 02'], '0.5')
self.assertEqual(data['HW 03'], '0.25')
self.assertEqual(data['HW Avg'], '0.5')
self.assertTrue('HW 04' not in data)
@patch('courseware.views.render_to_response', intercept_renderer)
def test_student_progress(self):
patch_context = patch('courseware.views.get_course_with_access')
get_course = patch_context.start()
get_course.return_value = self.course
self.addCleanup(patch_context.stop)
self.client.login(username=self.student.username, password="test")
url = reverse(
'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)
def flatten(seq): def flatten(seq):
...@@ -457,3 +511,26 @@ def flatten(seq): ...@@ -457,3 +511,26 @@ def flatten(seq):
For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse. For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse.
""" """
return [x for sub in seq for x in sub] return [x for sub in seq for x in sub]
def iter_blocks(course):
"""
Returns an iterator over all of the blocks in a course.
"""
def visit(block):
yield block
for child in block.get_children():
for descendant in visit(child): # wish they'd backport yield from
yield descendant
return visit(course)
def visible_children(block):
block_get_children = block.get_children
def get_children():
def iter_children():
for child in block_get_children():
child._field_data_cache = {}
if not child.visible_to_staff_only:
yield child
return list(iter_children())
block.get_children = get_children
...@@ -20,6 +20,8 @@ from django.contrib.auth.models import User ...@@ -20,6 +20,8 @@ from django.contrib.auth.models import User
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
from courseware.field_overrides import disable_overrides from courseware.field_overrides import disable_overrides
from courseware.grades import iterate_grades_for from courseware.grades import iterate_grades_for
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.roles import CoursePocCoachRole from student.roles import CoursePocCoachRole
...@@ -33,6 +35,7 @@ from .overrides import ( ...@@ -33,6 +35,7 @@ from .overrides import (
clear_override_for_poc, clear_override_for_poc,
get_override_for_poc, get_override_for_poc,
override_field_for_poc, override_field_for_poc,
poc_context,
) )
from .utils import enroll_email, unenroll_email from .utils import enroll_email, unenroll_email
...@@ -290,68 +293,90 @@ def poc_gradebook(request, course): ...@@ -290,68 +293,90 @@ def poc_gradebook(request, course):
""" """
Show the gradebook for this POC. Show the gradebook for this POC.
""" """
# Need course module for overrides to function properly
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2)
course = get_module_for_descriptor(
request.user, request, course, field_data_cache, course.id)
poc = get_poc_for_coach(course, request.user) poc = get_poc_for_coach(course, request.user)
enrolled_students = User.objects.filter( with poc_context(poc):
pocmembership__poc=poc, course._field_data_cache = {}
pocmembership__active=1 course.set_grading_policy(course.grading_policy) # this is so awful
).order_by('username').select_related("profile") enrolled_students = User.objects.filter(
pocmembership__poc=poc,
student_info = [ pocmembership__active=1
{ ).order_by('username').select_related("profile")
'username': student.username,
'id': student.id, student_info = [
'email': student.email, {
'grade_summary': student_grades(student, request, course), 'username': student.username,
'realname': student.profile.name, 'id': student.id,
} 'email': student.email,
for student in enrolled_students 'grade_summary': student_grades(student, request, course),
] 'realname': student.profile.name,
}
return render_to_response('courseware/gradebook.html', { for student in enrolled_students
'students': student_info, ]
'course': course,
'course_id': course.id, return render_to_response('courseware/gradebook.html', {
'staff_access': request.user.is_staff, 'students': student_info,
'ordered_grades': sorted( 'course': course,
course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True), '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) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@coach_dashboard @coach_dashboard
def poc_grades_csv(request, course): def poc_grades_csv(request, course):
poc = get_poc_for_coach(course, request.user) """
enrolled_students = User.objects.filter( Download grades as CSV.
pocmembership__poc=poc, """
pocmembership__active=1 # Need course module for overrides to function properly
).order_by('username').select_related("profile") field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
grades = iterate_grades_for(course.id, enrolled_students) course.id, request.user, course, depth=2)
course = get_module_for_descriptor(
header = None request.user, request, course, field_data_cache, course.id)
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') poc = get_poc_for_coach(course, request.user)
with poc_context(poc):
course._field_data_cache = {}
course.set_grading_policy(course.grading_policy) # this is so awful
enrolled_students = User.objects.filter(
pocmembership__poc=poc,
pocmembership__active=1
).order_by('username').select_related("profile")
grades = iterate_grades_for(course, 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/plain')
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