Commit d3128f4d by Eric Fischer Committed by GitHub

Masquerading staff override hidden special exams [TNL-4808] (#12806)

Allows staff using "view as specific student" mode to bypass edx-proctoring
hiding special exams from students, to allow for more useful debugging.

Includes "shim" pattern library implementation of alerts, and a bokchoy
test for this functionality.
parent 518a428f
......@@ -210,13 +210,15 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
self._capture_basic_metrics()
# Is this sequential part of a timed or proctored exam?
masquerading = context.get('specific_masquerade', False)
special_exam_html = None
if self.is_time_limited:
view_html = self._time_limited_student_view(context)
special_exam_html = self._time_limited_student_view(context)
# Do we have an alternate rendering
# Do we have an applicable alternate rendering
# from the edx_proctoring subsystem?
if view_html:
fragment.add_content(view_html)
if special_exam_html and not masquerading:
fragment.add_content(special_exam_html)
return fragment
for child in display_items:
......@@ -249,6 +251,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'ajax_url': self.system.ajax_url,
'next_url': context.get('next_url'),
'prev_url': context.get('prev_url'),
'override_hidden_exam': masquerading and special_exam_html is not None,
}
fragment.add_content(self.system.render_template("seq_module.html", params))
......
......@@ -17,6 +17,7 @@ from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage
from ...pages.lms.course_nav import CourseNavPage
from ...pages.lms.problem import ProblemPage
from ...pages.common.logout import LogoutPage
from ...pages.lms.staff_view import StaffPage
from ...pages.lms.track_selection import TrackSelectionPage
from ...pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from ...pages.lms.dashboard import DashboardPage
......@@ -249,22 +250,10 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page.visit()
self.assertTrue(self.courseware_page.can_start_proctored_exam)
@ddt.data(True, False)
def test_timed_exam_flow(self, hide_after_due):
def _setup_and_take_timed_exam(self, hide_after_due=False):
"""
Given that I am a staff member on the exam settings section
select advanced settings tab
When I Make the exam timed.
And I login as a verified student.
And visit the courseware as a verified student.
And I start the timed exam
Then I am taken to the exam with a timer bar showing
When I finish the exam
Then I see the exam submitted dialog in place of the exam
When I log back into studio as a staff member
And change the problem's due date to be in the past
And log back in as the original verified student
Then I see the exam or message in accordance with the hide_after_due setting
Helper to perform the common action "set up a timed exam as staff,
then take it as student"
"""
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
......@@ -285,6 +274,27 @@ class ProctoredExamTest(UniqueCourseTest):
self.assertTrue(self.courseware_page.has_submitted_exam_message())
LogoutPage(self.browser).visit()
@ddt.data(True, False)
def test_timed_exam_flow(self, hide_after_due):
"""
Given that I am a staff member on the exam settings section
select advanced settings tab
When I Make the exam timed.
And I login as a verified student.
And visit the courseware as a verified student.
And I start the timed exam
Then I am taken to the exam with a timer bar showing
When I finish the exam
Then I see the exam submitted dialog in place of the exam
When I log back into studio as a staff member
And change the problem's due date to be in the past
And log back in as the original verified student
Then I see the exam or message in accordance with the hide_after_due setting
"""
self._setup_and_take_timed_exam(hide_after_due)
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit()
last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y")
......@@ -295,6 +305,25 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page.visit()
self.assertEqual(self.courseware_page.has_submitted_exam_message(), hide_after_due)
def test_masquerade_visibility_override(self):
"""
Given that a timed exam problem exists in the course
And a student has taken that exam
And that exam is hidden to the student
And I am a staff user masquerading as the student
Then I should be able to see the exam content
"""
self._setup_and_take_timed_exam()
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.courseware_page.visit()
staff_page = StaffPage(self.browser, self.course_id)
self.assertEqual(staff_page.staff_view_mode, 'Staff')
staff_page.set_staff_view_mode_specific_student(self.USERNAME)
self.assertFalse(self.courseware_page.has_submitted_exam_message())
def test_field_visiblity_with_all_exam_types(self):
"""
Given that I am a staff member
......
......@@ -295,6 +295,12 @@ class CoursewareIndex(View):
"""
return self.masquerade and self.masquerade.role == 'student'
def _is_masquerading_as_specific_student(self):
"""
Returns whether the current request is masqueurading as a specific student.
"""
return self._is_masquerading_as_student() and self.masquerade.user_name
def _find_block(self, parent, url_name, block_type, min_depth=None):
"""
Finds the block in the parent with the specified url_name.
......@@ -464,6 +470,8 @@ class CoursewareIndex(View):
section_context['prev_url'] = _compute_section_url(previous_of_active_section, 'last')
if next_of_active_section:
section_context['next_url'] = _compute_section_url(next_of_active_section, 'first')
# sections can hide data that masquerading staff should see when debugging issues with specific students
section_context['specific_masquerade'] = self._is_masquerading_as_specific_student()
return section_context
def _handle_unexpected_error(self):
......
......@@ -7,6 +7,7 @@
@import 'base/headings';
@import 'base/extends';
@import 'base/animations';
@import 'shared/alerts_pattern_library_shim';
@import 'shared/tooltips';
// base - elements
......
......@@ -722,7 +722,7 @@
}
// adopted alerts
.alert {
.alert:not(.pattern-library-shim) {
@include box-sizing(border-box);
@include clearfix();
margin: 0 auto;
......
// lms alerts
// This file contains "I want alerts that look like the pattern library" sass
// and should be replaced when moving to the patter library proper
// https://github.com/edx/ux-pattern-library/blob/master/pattern-library/sass/patterns/_alerts.scss
// ------------------------------
// edX Pattern Library: Utilities - Alerts
//
// About: Contains base styling for alerts.
// ----------------------------
// #CONFIG
// #UTILITIES
// #GENERAL
// #INDIVIDUAL CASES
// ----------------------------
// #CONFIG
// ----------------------------
// alert colors
$alert-information-color: #6fa0ba;
$alert-warning-color: #fdbc56;
$alert-error-color: #b20610;
$alert-success-color: #25b85a;
$alert-background-color: #fcfcfc;
$alert-icon-color: #fcfcfc;
$alert-border-grey: #cdd7db;
$alert-shadow-grey: #eef1f2;
// alert borders
$alert-border-radius: 0.3125rem;
$alert-border: 0.0625rem solid $alert-border-grey;
// ----------------------------
// #UTILITIES
// ----------------------------
@mixin alert($alert-color) {
border-top: rem(2) solid $alert-color;
.alert-icon {
color: $alert-icon-color;
background-color: $alert-color;
}
}
@mixin alert-message($width) {
@include float(left);
width: $width;
padding: 1.25rem;
padding-top: 0;
padding-bottom: 0;
:last-child {
// keeps the message compact
margin-bottom: 0;
}
}
// everything below here gets added specificity pattern-library-shim
.pattern-library-shim {
// ----------------------------
// #GENERAL
// ----------------------------
&.alert {
background-color: $alert-background-color;
border: $alert-border;
border-radius: $alert-border-radius;
border-top-left-radius: 0;
border-top-right-radius: 0;
padding: 1.25rem;
overflow: auto;
box-shadow: 0 rem(2) rem(2) 0 $alert-shadow-grey;
}
&.alert-icon {
@include float(left);
display: block;
// create a circle around the icon
border-radius: 50%;
// create room around the icon for the circle
padding: 0.625rem;
}
&.alert-message-with-action {
// provide room for the action to be displayed next to the alert message
@include alert-message(70%);
}
&.alert-message {
@include alert-message(90%);
}
&.alert-title {
@extend %hd-5;
@extend %headings-emphasized;
// shift the section up to make the alert more compact
margin-top: -0.625rem;
}
&.alert-copy {
@extend %copy-base;
// shift the message down to be in line with the icon
margin-top: 0.3125rem;
}
&.alert-copy-with-title {
@extend %copy-base;
}
&.alert-action {
@include float(right);
width: inherit;
}
// ----------------------------
// #INDIVIDUAL CASES
// ----------------------------
// information-based alert
&.alert-information {
@include alert($alert-information-color);
}
// warning-based alert
&.alert-warning {
@include alert($alert-warning-color);
}
// error-based alert
&.alert-error {
@include alert($alert-error-color);
}
// success-based alert
&.alert-success {
@include alert($alert-success-color);
}
// added from _icons.scss
&.icon {
display: inline-block;
height: auto;
width: auto;
font-family: FontAwesome;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
&.icon-bullhorn:before {
content: "\f0a1";
}
// handles negative margin on navigation bar
&.subsection-header {
margin-top: -4px;
margin-bottom: 14px;
}
}
......@@ -3,6 +3,16 @@
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" data-next-url="${next_url}" data-prev-url="${prev_url}">
<div class="path"></div>
% if override_hidden_exam:
<div class="pattern-library-shim alert alert-information subsection-header" tabindex="-1">
<span class="pattern-library-shim icon alert-icon icon-bullhorn" aria-hidden="true"></span>
<div class="pattern-library-shim alert-message">
<p class="pattern-library-shim alert-copy">
${_("This exam is hidden from the learner.")}
</p>
</div>
</div>
% endif
<div class="sequence-nav">
<button class="sequence-nav-button button-previous">
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span><span class="sr">${_('Previous')}</span>
......
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