Commit 621a9355 by Calen Pennington

Merge pull request #1858 from MITx/feature/ichuang/masquerade-v3

Allow course staff to masquerade as students in viewing courseware
parents 57fde4ba 977eb653
...@@ -15,6 +15,7 @@ from xmodule.modulestore import Location ...@@ -15,6 +15,7 @@ from xmodule.modulestore import Location
from xmodule.x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed from student.models import CourseEnrollmentAllowed
from courseware.masquerade import is_masquerading_as_student
DEBUG_ACCESS = False DEBUG_ACCESS = False
...@@ -235,7 +236,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): ...@@ -235,7 +236,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
don't have to hit the enrollments table on every module load. don't have to hit the enrollments table on every module load.
""" """
# If start dates are off, can always load # If start dates are off, can always load
if settings.MITX_FEATURES['DISABLE_START_DATES']: if settings.MITX_FEATURES['DISABLE_START_DATES'] and not is_masquerading_as_student(user):
debug("Allow: DISABLE_START_DATES") debug("Allow: DISABLE_START_DATES")
return True return True
...@@ -543,6 +544,10 @@ def _has_access_to_location(user, location, access_level, course_context): ...@@ -543,6 +544,10 @@ def _has_access_to_location(user, location, access_level, course_context):
if user is None or (not user.is_authenticated()): if user is None or (not user.is_authenticated()):
debug("Deny: no user or anon user") debug("Deny: no user or anon user")
return False return False
if is_masquerading_as_student(user):
return False
if user.is_staff: if user.is_staff:
debug("Allow: user.is_staff") debug("Allow: user.is_staff")
return True return True
......
'''
---------------------------------------- Masequerade ----------------------------------------
Allow course staff to see a student or staff view of courseware.
Which kind of view has been selected is stored in the session state.
'''
import json
import logging
from django.http import HttpResponse
from django.conf import settings
log = logging.getLogger(__name__)
MASQ_KEY = 'masquerade_identity'
def handle_ajax(request, marg):
'''
Handle ajax call from "staff view" / "student view" toggle button
'''
if marg == 'toggle':
status = request.session.get(MASQ_KEY, '')
if status is None or status in ['', 'staff']:
status = 'student'
else:
status = 'staff'
request.session[MASQ_KEY] = status
return HttpResponse(json.dumps({'status': status}))
def setup_masquerade(request, staff_access=False):
'''
Setup masquerade identity (allows staff to view courseware as either staff or student)
Uses request.session[MASQ_KEY] to store status of masquerading.
Adds masquerade status to request.user, if masquerading active.
Return string version of status of view (either 'staff' or 'student')
'''
if request.user is None:
return None
if not settings.MITX_FEATURES.get('ENABLE_MASQUERADE', False):
return None
if not staff_access: # can masquerade only if user has staff access to course
return None
usertype = request.session.get(MASQ_KEY, '')
if usertype is None or not usertype:
request.session[MASQ_KEY] = 'staff'
usertype = 'staff'
if usertype == 'student':
request.user.masquerade_as_student = True
return usertype
def is_masquerading_as_student(user):
'''
Return True if user is masquerading as a student, False otherwise
'''
masq = getattr(user, 'masquerade_as_student', False)
return masq==True
...@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt ...@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface from capa.xqueue_interface import XQueueInterface
from courseware.masquerade import setup_masquerade
from courseware.access import has_access from courseware.access import has_access
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from .models import StudentModule from .models import StudentModule
...@@ -164,6 +165,10 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -164,6 +165,10 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
Actually implement get_module. See docstring there for details. Actually implement get_module. See docstring there for details.
""" """
# allow course staff to masquerade as student
if has_access(user, descriptor, 'staff', course_id):
setup_masquerade(request, True)
# Short circuit--if the user shouldn't have access, bail without doing any work # Short circuit--if the user shouldn't have access, bail without doing any work
if not has_access(user, descriptor, 'load', course_id): if not has_access(user, descriptor, 'load', course_id):
return None return None
......
"""
Unit tests for masquerade
Based on (and depends on) unit tests for courseware.
Notes for running by hand:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware
"""
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User, Group
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
import json
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
'''
Check for staff being able to masquerade as student
'''
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
#self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
#self.toy = modulestore().get_course("edX/toy/2012_Fall")
self.graded_course = modulestore().get_course("edX/graded/2012_Fall")
# Create staff account
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.instructor)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
make_instructor(self.graded_course)
self.logout()
self.login(self.instructor, self.password)
self.enroll(self.graded_course)
# self.factory = RequestFactory()
def get_cw_section(self):
url = reverse('courseware_section',
kwargs={'course_id': self.graded_course.id,
'chapter': 'GradedChapter',
'section': 'Homework1'})
resp = self.client.get(url)
print "url ", url
return resp
def test_staff_debug_for_staff(self):
resp = self.get_cw_section()
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
self.assertTrue(sdebug in resp.content)
def toggle_masquerade(self):
'''
Toggle masquerade state
'''
masq_url = reverse('masquerade-switch', kwargs={'marg': 'toggle'})
print "masq_url ", masq_url
resp = self.client.get(masq_url)
return resp
def test_no_staff_debug_for_student(self):
togresp = self.toggle_masquerade()
print "masq now ", togresp.content
self.assertEqual(togresp.content, '{"status": "student"}', '')
resp = self.get_cw_section()
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
self.assertFalse(sdebug in resp.content)
def get_problem(self):
pun = 'H1P1'
problem_location = "i4x://edX/graded/problem/%s" % pun
modx_url = reverse('modx_dispatch',
kwargs={'course_id': self.graded_course.id,
'location': problem_location,
'dispatch': 'problem_get', })
resp = self.client.get(modx_url)
print "modx_url ", modx_url
return resp
def test_showanswer_for_staff(self):
resp = self.get_problem()
html = json.loads(resp.content)['html']
print html
sabut = '<input class="show" type="button" value="Show Answer">'
self.assertTrue(sabut in html)
def test_no_showanswer_for_student(self):
togresp = self.toggle_masquerade()
print "masq now ", togresp.content
self.assertEqual(togresp.content, '{"status": "student"}', '')
resp = self.get_problem()
html = json.loads(resp.content)['html']
print html
sabut = '<input class="show" type="button" value="Show Answer">'
self.assertFalse(sabut in html)
...@@ -20,6 +20,7 @@ from courseware.access import has_access ...@@ -20,6 +20,7 @@ from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access, from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement) get_courses_by_university, sort_by_announcement)
import courseware.tabs as tabs import courseware.tabs as tabs
from courseware.masquerade import setup_masquerade
from courseware.model_data import ModelDataCache from courseware.model_data import ModelDataCache
from .module_render import toc_for_course, get_module_for_descriptor, get_module from .module_render import toc_for_course, get_module_for_descriptor, get_module
from courseware.models import StudentModule, StudentModuleHistory from courseware.models import StudentModule, StudentModuleHistory
...@@ -89,6 +90,7 @@ def render_accordion(request, course, chapter, section, model_data_cache): ...@@ -89,6 +90,7 @@ def render_accordion(request, course, chapter, section, model_data_cache):
# grab the table of contents # grab the table of contents
user = User.objects.prefetch_related("groups").get(id=request.user.id) user = User.objects.prefetch_related("groups").get(id=request.user.id)
request.user = user # keep just one instance of User
toc = toc_for_course(user, request, course, chapter, section, model_data_cache) toc = toc_for_course(user, request, course, chapter, section, model_data_cache)
context = dict([('toc', toc), context = dict([('toc', toc),
...@@ -260,6 +262,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -260,6 +262,7 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse - HTTPresponse
""" """
user = User.objects.prefetch_related("groups").get(id=request.user.id) user = User.objects.prefetch_related("groups").get(id=request.user.id)
request.user = user # keep just one instance of User
course = get_course_with_access(user, course_id, 'load', depth=2) course = get_course_with_access(user, course_id, 'load', depth=2)
staff_access = has_access(user, course, 'staff') staff_access = has_access(user, course, 'staff')
registered = registered_for_course(course, user) registered = registered_for_course(course, user)
...@@ -268,6 +271,8 @@ def index(request, course_id, chapter=None, section=None, ...@@ -268,6 +271,8 @@ def index(request, course_id, chapter=None, section=None,
log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url())) log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url()))
return redirect(reverse('about_course', args=[course.id])) return redirect(reverse('about_course', args=[course.id]))
masq = setup_masquerade(request, staff_access)
try: try:
model_data_cache = ModelDataCache.cache_for_descriptor_descendents( model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
course.id, user, course, depth=2) course.id, user, course, depth=2)
...@@ -289,6 +294,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -289,6 +294,7 @@ def index(request, course_id, chapter=None, section=None,
'init': '', 'init': '',
'content': '', 'content': '',
'staff_access': staff_access, 'staff_access': staff_access,
'masquerade': masq,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa') 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
} }
...@@ -301,12 +307,18 @@ def index(request, course_id, chapter=None, section=None, ...@@ -301,12 +307,18 @@ def index(request, course_id, chapter=None, section=None,
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter) chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
if chapter_module is None: if chapter_module is None:
# User may be trying to access a chapter that isn't live yet # User may be trying to access a chapter that isn't live yet
if masq=='student': # if staff is masquerading as student be kinder, don't 404
log.debug('staff masq as student: no chapter %s' % chapter)
return redirect(reverse('courseware', args=[course.id]))
raise Http404 raise Http404
if section is not None: if section is not None:
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section) section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
if section_descriptor is None: if section_descriptor is None:
# Specifically asked-for section doesn't exist # Specifically asked-for section doesn't exist
if masq=='student': # if staff is masquerading as student be kinder, don't 404
log.debug('staff masq as student: no section %s' % section)
return redirect(reverse('courseware', args=[course.id]))
raise Http404 raise Http404
# cdodge: this looks silly, but let's refetch the section_descriptor with depth=None # cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
...@@ -437,9 +449,10 @@ def course_info(request, course_id): ...@@ -437,9 +449,10 @@ def course_info(request, course_id):
""" """
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None, return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None,
'course': course, 'staff_access': staff_access}) 'course': course, 'staff_access': staff_access, 'masquerade': masq})
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -71,6 +71,8 @@ MITX_FEATURES = { ...@@ -71,6 +71,8 @@ MITX_FEATURES = {
'ENABLE_LMS_MIGRATION': False, 'ENABLE_LMS_MIGRATION': False,
'ENABLE_MANUAL_GIT_RELOAD': False, 'ENABLE_MANUAL_GIT_RELOAD': False,
'ENABLE_MASQUERADE': True, # allow course staff to change to student view of courseware
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL 'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
......
...@@ -27,6 +27,42 @@ def url_class(is_active): ...@@ -27,6 +27,42 @@ def url_class(is_active):
</li> </li>
% endfor % endfor
<%block name="extratabs" /> <%block name="extratabs" />
% if masquerade is not UNDEFINED:
% if staff_access and masquerade is not None:
<li style="float:right"><a href="#" id="staffstatus">Staff view</a></li>
% endif
% endif
</ol> </ol>
</div> </div>
</nav> </nav>
% if masquerade is not UNDEFINED:
% if staff_access and masquerade is not None:
<script type="text/javascript">
masq = (function(){
var el = $('#staffstatus');
var setstat = function(status){
if (status=='student'){
el.html('<font color="green">Student view</font>');
}else{
el.html('<font color="red">Staff view</font>');
}
}
setstat('${masquerade}');
el.click(function(){
$.ajax({ url: '/masquerade/toggle',
type: 'GET',
success: function(result){
setstat(result.status);
location.reload();
},
error: function() {
alert('Error: cannot connect to server');
}
});
});
}() );
</script>
% endif
% endif
...@@ -159,9 +159,6 @@ if settings.WIKI_ENABLED: ...@@ -159,9 +159,6 @@ if settings.WIKI_ENABLED:
if settings.COURSEWARE_ENABLED: if settings.COURSEWARE_ENABLED:
urlpatterns += ( urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users
url(r'^masquerade/', include('masquerade.urls')),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/jump_to/(?P<location>.*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/jump_to/(?P<location>.*)$',
'courseware.views.jump_to', name="jump_to"), 'courseware.views.jump_to', name="jump_to"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
...@@ -299,6 +296,13 @@ if settings.COURSEWARE_ENABLED: ...@@ -299,6 +296,13 @@ if settings.COURSEWARE_ENABLED:
'open_ended_grading.views.peer_grading', name='peer_grading'), 'open_ended_grading.views.peer_grading', name='peer_grading'),
) )
# allow course staff to change to student view of courseware
if settings.MITX_FEATURES.get('ENABLE_MASQUERADE'):
urlpatterns += (
url(r'^masquerade/(?P<marg>.*)$','courseware.masquerade.handle_ajax', name="masquerade-switch"),
)
# discussion forums live within courseware, so courseware must be enabled first # discussion forums live within courseware, so courseware must be enabled first
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += ( urlpatterns += (
......
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