Merge pull request #17830 from edx/noraiz/EDUCATOR-2587

permit staff role to rescore, override and delete problem scores for individual learners
This commit is contained in:
Noraiz Anwar
2018-04-05 19:32:09 +05:00
committed by GitHub
5 changed files with 66 additions and 35 deletions

View File

@@ -688,13 +688,11 @@ def get_module_system_for_user(
# the result would always be "False".
masquerade_settings = user.real_user.masquerade_settings
del user.real_user.masquerade_settings
instructor_access = bool(has_access(user.real_user, 'instructor', descriptor, course_id))
user.real_user.masquerade_settings = masquerade_settings
else:
staff_access = has_access(user, 'staff', descriptor, course_id)
instructor_access = bool(has_access(user, 'instructor', descriptor, course_id))
if staff_access:
block_wrappers.append(partial(add_staff_markup, user, instructor_access, disable_staff_debug_info))
block_wrappers.append(partial(add_staff_markup, user, disable_staff_debug_info))
# These modules store data using the anonymous_student_id as a key.
# To prevent loss of data, we will continue to provide old modules with

View File

@@ -205,6 +205,7 @@ INSTRUCTOR_POST_ENDPOINTS = set([
'spent_registration_codes',
'students_update_enrollment',
'update_forum_role_membership',
'override_problem_score',
])
@@ -386,11 +387,37 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
def setUpClass(cls):
super(TestInstructorAPIDenyLevels, cls).setUpClass()
cls.course = CourseFactory.create()
cls.problem_location = msk_from_problem_urlname(
cls.course.id,
'robot-some-problem-urlname'
cls.chapter = ItemFactory.create(
parent=cls.course,
category='chapter',
display_name="Chapter",
publish_item=True,
start=datetime.datetime(2018, 3, 10, tzinfo=UTC),
)
cls.problem_urlname = text_type(cls.problem_location)
cls.sequential = ItemFactory.create(
parent=cls.chapter,
category='sequential',
display_name="Lesson",
publish_item=True,
start=datetime.datetime(2018, 3, 10, tzinfo=UTC),
metadata={'graded': True, 'format': 'Homework'},
)
cls.vertical = ItemFactory.create(
parent=cls.sequential,
category='vertical',
display_name='Subsection',
publish_item=True,
start=datetime.datetime(2018, 3, 10, tzinfo=UTC),
)
cls.problem = ItemFactory.create(
category="problem",
parent=cls.vertical,
display_name="A Problem Block",
weight=1,
publish_item=True,
)
cls.problem_urlname = text_type(cls.problem.location)
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
@classmethod
@@ -406,7 +433,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
_module = StudentModule.objects.create(
student=self.user,
course_id=self.course.id,
module_state_key=self.problem_location,
module_state_key=self.problem.location,
state=json.dumps({'attempts': 10}),
)
@@ -417,8 +444,6 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
('get_grading_config', {}),
('get_students_features', {}),
('get_student_progress_url', {'unique_student_identifier': self.user.username}),
('reset_student_attempts',
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
('update_forum_role_membership',
{'unique_student_identifier': self.user.email, 'rolename': 'Moderator', 'action': 'allow'}),
('list_forum_members', {'rolename': FORUM_ROLE_COMMUNITY_TA}),
@@ -435,15 +460,28 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
('get_proctored_exam_results', {}),
('get_problem_responses', {}),
('export_ora2_data', {}),
('rescore_problem',
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
('override_problem_score',
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email, 'score': 0}),
('reset_student_attempts',
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
(
'reset_student_attempts',
{
'problem_to_reset': self.problem_urlname,
'unique_student_identifier': self.user.email,
'delete_module': True
}
),
]
# Endpoints that only Instructors can access
self.instructor_level_endpoints = [
('bulk_beta_modify_access', {'identifiers': 'foo@example.org', 'action': 'add'}),
('modify_access', {'unique_student_identifier': self.user.email, 'rolename': 'beta', 'action': 'allow'}),
('list_course_role_members', {'rolename': 'beta'}),
('rescore_problem',
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
('rescore_problem', {'problem_to_reset': self.problem_urlname, 'all_students': True}),
('reset_student_attempts', {'problem_to_reset': self.problem_urlname, 'all_students': True}),
]
def _access_endpoint(self, endpoint, args, status_code, msg):
@@ -569,10 +607,6 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
for endpoint, args in self.instructor_level_endpoints:
expected_status = 200
# TODO: make this work
if endpoint in ['rescore_problem']:
continue
self._access_endpoint(
endpoint,
args,

View File

@@ -1907,13 +1907,16 @@ def reset_student_attempts(request, course_id):
course = get_course_with_access(
request.user, 'staff', course_id, depth=None
)
all_students = _get_boolean_param(request, 'all_students')
if all_students and not has_access(request.user, 'instructor', course):
return HttpResponseForbidden("Requires instructor access.")
problem_to_reset = strip_if_string(request.POST.get('problem_to_reset'))
student_identifier = request.POST.get('unique_student_identifier', None)
student = None
if student_identifier is not None:
student = get_student_from_identifier(student_identifier)
all_students = _get_boolean_param(request, 'all_students')
delete_module = _get_boolean_param(request, 'delete_module')
# parameter combinations
@@ -1926,11 +1929,6 @@ def reset_student_attempts(request, course_id):
"all_students and delete_module are mutually exclusive."
)
# instructor authorization
if all_students or delete_module:
if not has_access(request.user, 'instructor', course):
return HttpResponseForbidden("Requires instructor access.")
try:
module_state_key = UsageKey.from_string(problem_to_reset).map_into_course(course_id)
except InvalidKeyError:
@@ -2044,13 +2042,13 @@ def reset_student_attempts_for_entrance_exam(request, course_id): # pylint: dis
@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('instructor')
@require_level('staff')
@require_post_params(problem_to_reset="problem urlname to reset")
@common_exceptions_400
def rescore_problem(request, course_id):
"""
Starts a background process a students attempts counter. Optionally deletes student state for a problem.
Limited to instructor access.
Rescore for all students is limited to instructor access.
Takes either of the following query paremeters
- problem_to_reset is a urlname of a problem
@@ -2060,15 +2058,19 @@ def rescore_problem(request, course_id):
all_students and unique_student_identifier cannot both be present.
"""
course_id = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'staff', course_id)
all_students = _get_boolean_param(request, 'all_students')
if all_students and not has_access(request.user, 'instructor', course):
return HttpResponseForbidden("Requires instructor access.")
only_if_higher = _get_boolean_param(request, 'only_if_higher')
problem_to_reset = strip_if_string(request.POST.get('problem_to_reset'))
student_identifier = request.POST.get('unique_student_identifier', None)
student = None
if student_identifier is not None:
student = get_student_from_identifier(student_identifier)
all_students = _get_boolean_param(request, 'all_students')
only_if_higher = _get_boolean_param(request, 'only_if_higher')
if not (problem_to_reset and (all_students or student)):
return HttpResponseBadRequest("Missing query parameters.")
@@ -2116,7 +2118,7 @@ def rescore_problem(request, course_id):
@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('instructor')
@require_level('staff')
@require_post_params(problem_to_reset="problem urlname to reset", score='overriding score')
@common_exceptions_400
def override_problem_score(request, course_id):
@@ -2142,7 +2144,7 @@ def override_problem_score(request, course_id):
return _create_error_response(request, "Unable to parse problem id {}.".format(problem_to_reset))
# check the user's access to this specific problem
if not has_access(request.user, "instructor", modulestore().get_item(usage_key)):
if not has_access(request.user, "staff", modulestore().get_item(usage_key)):
_create_error_response(request, "User {} does not have permission to override scores for problem {}.".format(
request.user.id,
problem_to_reset

View File

@@ -80,7 +80,6 @@ ${block_content}
<button type="button" class="btn-link staff-debug-reset">${_('Reset Learner\'s Attempts to Zero')}</button>
|
% endif
% if has_instructor_access:
<button type="button" class="btn-link staff-debug-sdelete">${_('Delete Learner\'s State')}</button>
% if can_rescore_problem:
|
@@ -92,7 +91,6 @@ ${block_content}
% if can_override_problem_score:
<button type="button" class="btn-link staff-debug-override-score">${_('Override Score')}</button>
% endif
% endif
]
</div>
<div id="result_${location.block_id | h}"></div>

View File

@@ -288,8 +288,8 @@ def sanitize_html_id(html_id):
return sanitized_html_id
@contract(user=User, has_instructor_access=bool, block=XBlock, view=basestring, frag=Fragment, context="dict|None")
def add_staff_markup(user, has_instructor_access, disable_staff_debug_info, block, view, frag, context): # pylint: disable=unused-argument
@contract(user=User, block=XBlock, view=basestring, frag=Fragment, context="dict|None")
def add_staff_markup(user, disable_staff_debug_info, block, view, frag, context): # pylint: disable=unused-argument
"""
Updates the supplied module with a new get_html function that wraps
the output of the old get_html function with additional information
@@ -383,7 +383,6 @@ def add_staff_markup(user, has_instructor_access, disable_staff_debug_info, bloc
'render_histogram': render_histogram,
'block_content': frag.content,
'is_released': is_released,
'has_instructor_access': has_instructor_access,
'can_reset_attempts': 'attempts' in block.fields,
'can_rescore_problem': hasattr(block, 'rescore'),
'can_override_problem_score': isinstance(block, ScorableXBlockMixin),