Merge pull request #8775 from open-craft/smarnach/masquerade
Allow staff to masquerade as a specific user in the LMS (SOL-816)
This commit is contained in:
@@ -80,6 +80,8 @@ class ABTestModule(ABTestFields, XModule):
|
||||
class ABTestDescriptor(ABTestFields, RawDescriptor, XmlDescriptor):
|
||||
module_class = ABTestModule
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
|
||||
@@ -119,6 +119,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
|
||||
module_class = CapaModule
|
||||
|
||||
has_score = True
|
||||
show_in_read_only_mode = True
|
||||
template_dir_name = 'problem'
|
||||
mako_template = "widgets/problem-edit.html"
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
|
||||
|
||||
@@ -188,6 +188,8 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
|
||||
|
||||
has_score = False
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Create an instance of the conditional module.
|
||||
|
||||
@@ -188,6 +188,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
|
||||
|
||||
has_score = True
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: d
|
||||
filename_extension = "xml"
|
||||
template_dir_name = "html"
|
||||
has_responsive_ui = True
|
||||
show_in_read_only_mode = True
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
@@ -299,6 +299,8 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
|
||||
js_module_name = "VerticalDescriptor"
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(LibraryContentDescriptor, self).non_editable_metadata_fields
|
||||
|
||||
@@ -101,6 +101,8 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
|
||||
|
||||
filename_extension = "xml"
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
|
||||
xml_object = etree.Element('randomize')
|
||||
|
||||
@@ -157,6 +157,8 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
mako_template = 'widgets/sequence-edit.html'
|
||||
module_class = SequenceModule
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
js = {
|
||||
'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')],
|
||||
}
|
||||
|
||||
@@ -375,6 +375,8 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
|
||||
|
||||
mako_template = "widgets/metadata-only-edit.html"
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
child_descriptor = module_attr('child_descriptor')
|
||||
log_child_render = module_attr('log_child_render')
|
||||
get_content_titles = module_attr('get_content_titles')
|
||||
|
||||
@@ -29,6 +29,8 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
|
||||
|
||||
has_children = True
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
def student_view(self, context):
|
||||
"""
|
||||
Renders the student view of the block in the LMS.
|
||||
|
||||
@@ -339,6 +339,8 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
|
||||
module_class = VideoModule
|
||||
transcript = module_attr('transcript')
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
tabs = [
|
||||
{
|
||||
'name': _("Basic"),
|
||||
|
||||
@@ -278,6 +278,10 @@ class XModuleMixin(XModuleFields, XBlock):
|
||||
# (like a practice problem).
|
||||
has_score = False
|
||||
|
||||
# Whether this module can be displayed in read-only mode. It is safe to set this to True if
|
||||
# all user state is handled through the FieldData API.
|
||||
show_in_read_only_mode = False
|
||||
|
||||
# Class level variable
|
||||
|
||||
# True if this descriptor always requires recalculation of grades, for
|
||||
@@ -754,6 +758,7 @@ class XModule(HTMLSnippet, XModuleMixin): # pylint: disable=abstract-method
|
||||
entry_point = "xmodule.v1"
|
||||
|
||||
has_score = descriptor_attr('has_score')
|
||||
show_in_read_only_mode = descriptor_attr('show_in_read_only_mode')
|
||||
_field_data_cache = descriptor_attr('_field_data_cache')
|
||||
_field_data = descriptor_attr('_field_data')
|
||||
_dirty_fields = descriptor_attr('_dirty_fields')
|
||||
|
||||
@@ -8,10 +8,15 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_POST
|
||||
from student.models import CourseEnrollment
|
||||
from util.json_request import expect_json, JsonResponse
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xblock.fragment import Fragment
|
||||
from xblock.runtime import KeyValueStore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,16 +24,21 @@ log = logging.getLogger(__name__)
|
||||
# The value is a dict from course keys to CourseMasquerade objects.
|
||||
MASQUERADE_SETTINGS_KEY = 'masquerade_settings'
|
||||
|
||||
# The key used to store temporary XBlock field data in the Django session. This is where field
|
||||
# data is stored to avoid modifying the state of the user we are masquerading as.
|
||||
MASQUERADE_DATA_KEY = 'masquerade_data'
|
||||
|
||||
|
||||
class CourseMasquerade(object):
|
||||
"""
|
||||
Masquerade settings for a particular course.
|
||||
"""
|
||||
def __init__(self, course_key, role='student', user_partition_id=None, group_id=None):
|
||||
def __init__(self, course_key, role='student', user_partition_id=None, group_id=None, user_name=None):
|
||||
self.course_key = course_key
|
||||
self.role = role
|
||||
self.user_partition_id = user_partition_id
|
||||
self.group_id = group_id
|
||||
self.user_name = user_name
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -46,40 +56,73 @@ def handle_ajax(request, course_key_string):
|
||||
role = request_json.get('role', 'student')
|
||||
user_partition_id = request_json.get('user_partition_id', None)
|
||||
group_id = request_json.get('group_id', None)
|
||||
user_name = request_json.get('user_name', None)
|
||||
if user_name:
|
||||
users_in_course = CourseEnrollment.objects.users_enrolled_in(course_key)
|
||||
try:
|
||||
if '@' in user_name:
|
||||
user_name = users_in_course.get(email=user_name).username
|
||||
else:
|
||||
users_in_course.get(username=user_name)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': _(
|
||||
'There is no user with the username or email address {user_name} '
|
||||
'enrolled in this course.'
|
||||
).format(user_name=user_name)
|
||||
})
|
||||
masquerade_settings[course_key] = CourseMasquerade(
|
||||
course_key,
|
||||
role=role,
|
||||
user_partition_id=user_partition_id,
|
||||
group_id=group_id
|
||||
group_id=group_id,
|
||||
user_name=user_name,
|
||||
)
|
||||
request.session[MASQUERADE_SETTINGS_KEY] = masquerade_settings
|
||||
return JsonResponse()
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
def setup_masquerade(request, course_key, staff_access=False):
|
||||
def setup_masquerade(request, course_key, staff_access=False, reset_masquerade_data=False):
|
||||
"""
|
||||
Sets up masquerading for the current user within the current request. The
|
||||
request's user is updated to have a 'masquerade_settings' attribute with
|
||||
the dict of all masqueraded settings if called from within a request context.
|
||||
The function then returns the CourseMasquerade object for the specified
|
||||
course key, or None if there isn't one.
|
||||
Sets up masquerading for the current user within the current request. The request's user is
|
||||
updated to have a 'masquerade_settings' attribute with the dict of all masqueraded settings if
|
||||
called from within a request context. The function then returns a pair (CourseMasquerade, User)
|
||||
with the masquerade settings for the specified course key or None if there isn't one, and the
|
||||
user we are masquerading as or request.user if masquerading as a specific user is not active.
|
||||
|
||||
If the reset_masquerade_data flag is set, the field data stored in the session will be cleared.
|
||||
"""
|
||||
if request.user is None:
|
||||
return None
|
||||
|
||||
if not settings.FEATURES.get('ENABLE_MASQUERADE', False):
|
||||
return None
|
||||
|
||||
if not staff_access: # can masquerade only if user has staff access to course
|
||||
return None
|
||||
|
||||
masquerade_settings = request.session.get(MASQUERADE_SETTINGS_KEY, {})
|
||||
|
||||
if (
|
||||
request.user is None or
|
||||
not settings.FEATURES.get('ENABLE_MASQUERADE', False) or
|
||||
not staff_access
|
||||
):
|
||||
return None, request.user
|
||||
if reset_masquerade_data:
|
||||
request.session.pop(MASQUERADE_DATA_KEY, None)
|
||||
masquerade_settings = request.session.setdefault(MASQUERADE_SETTINGS_KEY, {})
|
||||
# Store the masquerade settings on the user so it can be accessed without the request
|
||||
request.user.masquerade_settings = masquerade_settings
|
||||
|
||||
# Return the masquerade for the current course, or none if there isn't one
|
||||
return masquerade_settings.get(course_key, None)
|
||||
course_masquerade = masquerade_settings.get(course_key, None)
|
||||
masquerade_user = None
|
||||
if course_masquerade and course_masquerade.user_name:
|
||||
try:
|
||||
masquerade_user = CourseEnrollment.objects.users_enrolled_in(course_key).get(
|
||||
username=course_masquerade.user_name
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
# This can only happen if the user was unenrolled from the course since masquerading
|
||||
# was enabled. We silently reset the masquerading configuration in this case.
|
||||
course_masquerade = None
|
||||
del masquerade_settings[course_key]
|
||||
request.session.modified = True
|
||||
else:
|
||||
# Store the masquerading settings on the masquerade_user as well, since this user will
|
||||
# be used in some places instead of request.user.
|
||||
masquerade_user.masquerade_settings = request.user.masquerade_settings
|
||||
masquerade_user.real_user = request.user
|
||||
return course_masquerade, masquerade_user or request.user
|
||||
|
||||
|
||||
def get_course_masquerade(user, course_key):
|
||||
@@ -106,6 +149,14 @@ def is_masquerading_as_student(user, course_key):
|
||||
return get_masquerade_role(user, course_key) == 'student'
|
||||
|
||||
|
||||
def is_masquerading_as_specific_student(user, course_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Returns whether the user is a staff member masquerading as a specific student.
|
||||
"""
|
||||
course_masquerade = get_course_masquerade(user, course_key)
|
||||
return bool(course_masquerade and course_masquerade.user_name)
|
||||
|
||||
|
||||
def get_masquerading_group_info(user, course_key):
|
||||
"""
|
||||
If the user is masquerading as belonging to a group, then this method returns
|
||||
@@ -116,3 +167,77 @@ def get_masquerading_group_info(user, course_key):
|
||||
if not course_masquerade:
|
||||
return None, None
|
||||
return course_masquerade.group_id, course_masquerade.user_partition_id
|
||||
|
||||
|
||||
# Sentinel object to mark deleted objects in the session cache
|
||||
_DELETED_SENTINEL = object()
|
||||
|
||||
|
||||
class MasqueradingKeyValueStore(KeyValueStore):
|
||||
"""
|
||||
A `KeyValueStore` to avoid affecting the user state when masquerading.
|
||||
|
||||
This `KeyValueStore` wraps an underlying `KeyValueStore`. Reads are forwarded to the underlying
|
||||
store, but writes go to a Django session (or other dictionary-like object).
|
||||
"""
|
||||
def __init__(self, kvs, session):
|
||||
"""
|
||||
Arguments:
|
||||
kvs: The KeyValueStore to wrap.
|
||||
session: The Django session used to store temporary data in.
|
||||
"""
|
||||
self.kvs = kvs
|
||||
self.session = session
|
||||
self.session_data = session.setdefault(MASQUERADE_DATA_KEY, {})
|
||||
|
||||
def _serialize_key(self, key):
|
||||
"""
|
||||
Convert the key of Type KeyValueStore.Key to a string.
|
||||
|
||||
Keys are not JSON-serializable, so we can't use them as keys for the Django session.
|
||||
The implementation is taken from cms/djangoapps/contentstore/views/session_kv_store.py.
|
||||
"""
|
||||
return repr(tuple(key))
|
||||
|
||||
def get(self, key):
|
||||
key_str = self._serialize_key(key)
|
||||
try:
|
||||
value = self.session_data[key_str]
|
||||
except KeyError:
|
||||
return self.kvs.get(key)
|
||||
else:
|
||||
if value is _DELETED_SENTINEL:
|
||||
raise KeyError(key_str)
|
||||
return value
|
||||
|
||||
def set(self, key, value):
|
||||
self.session_data[self._serialize_key(key)] = value
|
||||
self.session.modified = True
|
||||
|
||||
def delete(self, key):
|
||||
# We can't simply delete the key from the session, since it might still exist in the kvs,
|
||||
# which we are not allowed to modify, so we mark it as deleted by setting it to
|
||||
# _DELETED_SENTINEL in the session.
|
||||
self.set(key, _DELETED_SENTINEL)
|
||||
|
||||
def has(self, key):
|
||||
try:
|
||||
value = self.session_data[self._serialize_key(key)]
|
||||
except KeyError:
|
||||
return self.kvs.has(key)
|
||||
else:
|
||||
return value != _DELETED_SENTINEL
|
||||
|
||||
|
||||
def filter_displayed_blocks(block, unused_view, frag, unused_context):
|
||||
"""
|
||||
A wrapper to only show XBlocks that set `show_in_read_only_mode` when masquerading as a specific user.
|
||||
|
||||
We don't want to modify the state of the user we are masquerading as, so we can't show XBlocks
|
||||
that store information outside of the XBlock fields API.
|
||||
"""
|
||||
if getattr(block, 'show_in_read_only_mode', False):
|
||||
return frag
|
||||
return Fragment(
|
||||
_(u'This type of component cannot be shown while viewing the course as a specific student.')
|
||||
)
|
||||
|
||||
@@ -480,27 +480,6 @@ class UserStateCache(object):
|
||||
kvs_key.field_name in self._cache[cache_key]
|
||||
)
|
||||
|
||||
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
|
||||
def set_score(self, user_id, usage_key, score, max_score):
|
||||
"""
|
||||
UNSUPPORTED METHOD
|
||||
|
||||
Set the score and max_score for the specified user and xblock usage.
|
||||
"""
|
||||
student_module, created = StudentModule.objects.get_or_create(
|
||||
student_id=user_id,
|
||||
module_state_key=usage_key,
|
||||
course_id=usage_key.course_key,
|
||||
defaults={
|
||||
'grade': score,
|
||||
'max_grade': max_score,
|
||||
}
|
||||
)
|
||||
if not created:
|
||||
student_module.grade = score
|
||||
student_module.max_grade = max_score
|
||||
student_module.save()
|
||||
|
||||
def __len__(self):
|
||||
return len(self._cache)
|
||||
|
||||
@@ -923,18 +902,6 @@ class FieldDataCache(object):
|
||||
|
||||
return self.cache[key.scope].has(key)
|
||||
|
||||
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
|
||||
def set_score(self, user_id, usage_key, score, max_score):
|
||||
"""
|
||||
UNSUPPORTED METHOD
|
||||
|
||||
Set the score and max_score for the specified user and xblock usage.
|
||||
"""
|
||||
assert not self.user.is_anonymous()
|
||||
assert user_id == self.user.id
|
||||
assert usage_key.course_key == self.course_id
|
||||
self.cache[Scope.user_state].set_score(user_id, usage_key, score, max_score)
|
||||
|
||||
@contract(key=DjangoKeyValueStore.Key, returns="datetime|None")
|
||||
def last_modified(self, key):
|
||||
"""
|
||||
@@ -1017,3 +984,23 @@ class ScoresClient(object):
|
||||
client = cls(fd_cache.course_id, fd_cache.user.id)
|
||||
client.fetch_scores(fd_cache.scorable_locations)
|
||||
return client
|
||||
|
||||
|
||||
# @contract(user_id=int, usage_key=UsageKey, score="number|None", max_score="number|None")
|
||||
def set_score(user_id, usage_key, score, max_score):
|
||||
"""
|
||||
Set the score and max_score for the specified user and xblock usage.
|
||||
"""
|
||||
student_module, created = StudentModule.objects.get_or_create(
|
||||
student_id=user_id,
|
||||
module_state_key=usage_key,
|
||||
course_id=usage_key.course_key,
|
||||
defaults={
|
||||
'grade': score,
|
||||
'max_grade': max_score,
|
||||
}
|
||||
)
|
||||
if not created:
|
||||
student_module.grade = score
|
||||
student_module.max_grade = max_score
|
||||
student_module.save()
|
||||
|
||||
@@ -28,8 +28,13 @@ import newrelic.agent
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.access import has_access, get_user_role
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
|
||||
from courseware.masquerade import (
|
||||
MasqueradingKeyValueStore,
|
||||
filter_displayed_blocks,
|
||||
is_masquerading_as_specific_student,
|
||||
setup_masquerade,
|
||||
)
|
||||
from courseware.model_data import DjangoKeyValueStore, FieldDataCache, set_score
|
||||
from courseware.models import SCORE_CHANGED
|
||||
from courseware.entrance_exams import (
|
||||
get_entrance_exam_score,
|
||||
@@ -113,7 +118,7 @@ def make_track_function(request):
|
||||
return function
|
||||
|
||||
|
||||
def toc_for_course(request, course, active_chapter, active_section, field_data_cache):
|
||||
def toc_for_course(user, request, course, active_chapter, active_section, field_data_cache):
|
||||
'''
|
||||
Create a table of contents from the module store
|
||||
|
||||
@@ -139,7 +144,7 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c
|
||||
|
||||
with modulestore().bulk_operations(course.id):
|
||||
course_module = get_module_for_descriptor(
|
||||
request.user, request, course, field_data_cache, course.id, course=course
|
||||
user, request, course, field_data_cache, course.id, course=course
|
||||
)
|
||||
if course_module is None:
|
||||
return None
|
||||
@@ -148,10 +153,10 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c
|
||||
chapters = course_module.get_display_items()
|
||||
|
||||
# See if the course is gated by one or more content milestones
|
||||
required_content = milestones_helpers.get_required_content(course, request.user)
|
||||
required_content = milestones_helpers.get_required_content(course, user)
|
||||
|
||||
# The user may not actually have to complete the entrance exam, if one is required
|
||||
if not user_must_complete_entrance_exam(request, request.user, course):
|
||||
if not user_must_complete_entrance_exam(request, user, course):
|
||||
required_content = [content for content in required_content if not content == course.entrance_exam_id]
|
||||
|
||||
for chapter in chapters:
|
||||
@@ -270,10 +275,15 @@ def get_module_for_descriptor(user, request, descriptor, field_data_cache, cours
|
||||
|
||||
user_location = getattr(request, 'session', {}).get('country_code')
|
||||
|
||||
student_kvs = DjangoKeyValueStore(field_data_cache)
|
||||
if is_masquerading_as_specific_student(user, course_key):
|
||||
student_kvs = MasqueradingKeyValueStore(student_kvs, request.session)
|
||||
student_data = KvsFieldData(student_kvs)
|
||||
|
||||
return get_module_for_descriptor_internal(
|
||||
user=user,
|
||||
descriptor=descriptor,
|
||||
field_data_cache=field_data_cache,
|
||||
student_data=student_data,
|
||||
course_id=course_key,
|
||||
track_function=track_function,
|
||||
xqueue_callback_url_prefix=xqueue_callback_url_prefix,
|
||||
@@ -288,7 +298,7 @@ def get_module_for_descriptor(user, request, descriptor, field_data_cache, cours
|
||||
)
|
||||
|
||||
|
||||
def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disable=too-many-statements
|
||||
def get_module_system_for_user(user, student_data, # TODO # pylint: disable=too-many-statements
|
||||
# Arguments preceding this comment have user binding, those following don't
|
||||
descriptor, course_id, track_function, xqueue_callback_url_prefix,
|
||||
request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
@@ -302,7 +312,7 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
closures that feed the instantiation of ModuleSystem.
|
||||
|
||||
The arguments fall into two categories: those that have explicit or implicit user binding, which are user
|
||||
and field_data_cache, and those don't and are just present so that ModuleSystem can be instantiated, which
|
||||
and student_data, and those don't and are just present so that ModuleSystem can be instantiated, which
|
||||
are all the other arguments. Ultimately, this isn't too different than how get_module_for_descriptor_internal
|
||||
was before refactoring.
|
||||
|
||||
@@ -313,7 +323,6 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
Returns:
|
||||
(LmsModuleSystem, KvsFieldData): (module system, student_data) bound to, primarily, the user and descriptor
|
||||
"""
|
||||
student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache))
|
||||
|
||||
def make_xqueue_callback(dispatch='score_update'):
|
||||
"""
|
||||
@@ -378,7 +387,7 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
return get_module_for_descriptor_internal(
|
||||
user=user,
|
||||
descriptor=descriptor,
|
||||
field_data_cache=field_data_cache,
|
||||
student_data=student_data,
|
||||
course_id=course_id,
|
||||
track_function=track_function,
|
||||
xqueue_callback_url_prefix=xqueue_callback_url_prefix,
|
||||
@@ -430,7 +439,7 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
grade = event.get('value')
|
||||
max_grade = event.get('max_value')
|
||||
|
||||
field_data_cache.set_score(
|
||||
set_score(
|
||||
user_id,
|
||||
descriptor.location,
|
||||
grade,
|
||||
@@ -472,7 +481,7 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
|
||||
def publish(block, event_type, event):
|
||||
"""A function that allows XModules to publish events."""
|
||||
if event_type == 'grade':
|
||||
if event_type == 'grade' and not is_masquerading_as_specific_student(user, course_id):
|
||||
handle_grade_event(block, event_type, event)
|
||||
else:
|
||||
track_function(event_type, event)
|
||||
@@ -502,10 +511,11 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
module.descriptor,
|
||||
asides=XBlockAsidesConfig.possible_asides(),
|
||||
)
|
||||
student_data_real_user = KvsFieldData(DjangoKeyValueStore(field_data_cache_real_user))
|
||||
|
||||
(inner_system, inner_student_data) = get_module_system_for_user(
|
||||
user=real_user,
|
||||
field_data_cache=field_data_cache_real_user, # These have implicit user bindings, rest of args considered not to
|
||||
student_data=student_data_real_user, # These have implicit user bindings, rest of args considered not to
|
||||
descriptor=module.descriptor,
|
||||
course_id=course_id,
|
||||
track_function=track_function,
|
||||
@@ -540,6 +550,9 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
# to the Fragment content coming out of the xblocks that are about to be rendered.
|
||||
block_wrappers = []
|
||||
|
||||
if is_masquerading_as_specific_student(user, course_id):
|
||||
block_wrappers.append(filter_displayed_blocks)
|
||||
|
||||
if settings.FEATURES.get("LICENSING", False):
|
||||
block_wrappers.append(wrap_with_license)
|
||||
|
||||
@@ -582,9 +595,23 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
))
|
||||
|
||||
if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'):
|
||||
if has_access(user, 'staff', descriptor, course_id):
|
||||
has_instructor_access = has_access(user, 'instructor', descriptor, course_id)
|
||||
block_wrappers.append(partial(add_staff_markup, user, has_instructor_access, disable_staff_debug_info))
|
||||
if is_masquerading_as_specific_student(user, course_id):
|
||||
# When masquerading as a specific student, we want to show the debug button
|
||||
# unconditionally to enable resetting the state of the student we are masquerading as.
|
||||
# We already know the user has staff access when masquerading is active.
|
||||
staff_access = True
|
||||
# To figure out whether the user has instructor access, we temporarily remove the
|
||||
# masquerade_settings from the real_user. With the masquerading settings in place,
|
||||
# the result would always be "False".
|
||||
masquerade_settings = user.real_user.masquerade_settings
|
||||
del user.real_user.masquerade_settings
|
||||
instructor_access = 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 = has_access(user, 'instructor', descriptor, course_id)
|
||||
if staff_access:
|
||||
block_wrappers.append(partial(add_staff_markup, user, instructor_access, 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
|
||||
@@ -691,7 +718,7 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
|
||||
|
||||
# TODO: Find all the places that this method is called and figure out how to
|
||||
# get a loaded course passed into it
|
||||
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, # pylint: disable=invalid-name
|
||||
def get_module_for_descriptor_internal(user, descriptor, student_data, course_id, # pylint: disable=invalid-name
|
||||
track_function, xqueue_callback_url_prefix, request_token,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path='', user_location=None, disable_staff_debug_info=False,
|
||||
@@ -707,7 +734,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
|
||||
|
||||
(system, student_data) = get_module_system_for_user(
|
||||
user=user,
|
||||
field_data_cache=field_data_cache, # These have implicit user bindings, the rest of args are considered not to
|
||||
student_data=student_data, # These have implicit user bindings, the rest of args are considered not to
|
||||
descriptor=descriptor,
|
||||
course_id=course_id,
|
||||
track_function=track_function,
|
||||
@@ -908,12 +935,12 @@ def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_inf
|
||||
tracking_context['module']['original_usage_key'] = unicode(descriptor_orig_usage_key)
|
||||
tracking_context['module']['original_usage_version'] = unicode(descriptor_orig_version)
|
||||
|
||||
unused_masquerade, user = setup_masquerade(request, course_id, has_access(user, 'staff', descriptor, course_id))
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course_id,
|
||||
user,
|
||||
descriptor
|
||||
)
|
||||
setup_masquerade(request, course_id, has_access(user, 'staff', descriptor, course_id))
|
||||
instance = get_module_for_descriptor(
|
||||
user,
|
||||
request,
|
||||
|
||||
@@ -528,6 +528,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
self.entrance_exam
|
||||
)
|
||||
return toc_for_course(
|
||||
self.request.user,
|
||||
self.request,
|
||||
self.course,
|
||||
self.entrance_exam.url_name,
|
||||
|
||||
@@ -7,13 +7,21 @@ from nose.plugins.attrib import attr
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory
|
||||
from courseware.masquerade import handle_ajax, setup_masquerade, get_masquerading_group_info
|
||||
from courseware.masquerade import (
|
||||
MasqueradingKeyValueStore,
|
||||
handle_ajax,
|
||||
setup_masquerade,
|
||||
get_masquerading_group_info
|
||||
)
|
||||
from courseware.tests.factories import StaffFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase, get_request_for_user
|
||||
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from student.tests.factories import UserFactory
|
||||
from xblock.runtime import DictKeyValueStore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
@@ -54,7 +62,7 @@ class MasqueradeTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
options=['Correct', 'Incorrect'],
|
||||
correct_option='Correct'
|
||||
)
|
||||
self.problem_display_name = "Test Masquerade Problem"
|
||||
self.problem_display_name = "TestMasqueradeProblem"
|
||||
self.problem = ItemFactory.create(
|
||||
parent_location=self.vertical.location,
|
||||
category='problem',
|
||||
@@ -158,7 +166,7 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
|
||||
"""
|
||||
return StaffFactory(course_key=self.course.id)
|
||||
|
||||
def update_masquerade(self, role, group_id=None):
|
||||
def update_masquerade(self, role, group_id=None, user_name=None):
|
||||
"""
|
||||
Toggle masquerade state.
|
||||
"""
|
||||
@@ -170,10 +178,10 @@ class StaffMasqueradeTestCase(MasqueradeTestCase):
|
||||
)
|
||||
response = self.client.post(
|
||||
masquerade_url,
|
||||
json.dumps({"role": role, "group_id": group_id}),
|
||||
json.dumps({"role": role, "group_id": group_id, "user_name": user_name}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
|
||||
@@ -215,6 +223,80 @@ class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
|
||||
self.verify_show_answer_present(True)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmissionTestMixin):
|
||||
"""
|
||||
Check for staff being able to masquerade as a specific student.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestStaffMasqueradeAsSpecificStudent, self).setUp()
|
||||
self.student_user = self.create_user()
|
||||
self.login_student()
|
||||
self.enroll(self.course, True)
|
||||
|
||||
def login_staff(self):
|
||||
""" Login as a staff user """
|
||||
self.login(self.test_user.email, 'test')
|
||||
|
||||
def login_student(self):
|
||||
""" Login as a student """
|
||||
self.login(self.student_user.email, 'test')
|
||||
|
||||
def submit_answer(self, response1, response2):
|
||||
"""
|
||||
Submit an answer to the single problem in our test course.
|
||||
"""
|
||||
return self.submit_question_answer(
|
||||
self.problem_display_name,
|
||||
{'2_1': response1, '2_2': response2}
|
||||
)
|
||||
|
||||
def get_progress_detail(self):
|
||||
"""
|
||||
Return the reported progress detail for the problem in our test course.
|
||||
|
||||
The return value is a string like u'1/2'.
|
||||
"""
|
||||
return json.loads(self.look_at_question(self.problem_display_name).content)['progress_detail']
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_masquerade_as_specific_student(self):
|
||||
"""
|
||||
Test masquerading as a specific user.
|
||||
|
||||
We answer the problem in our test course as the student and as staff user, and we use the
|
||||
progress as a proxy to determine who's state we currently see.
|
||||
"""
|
||||
# Answer correctly as the student, and check progress.
|
||||
self.login_student()
|
||||
self.submit_answer('Correct', 'Correct')
|
||||
self.assertEqual(self.get_progress_detail(), u'2/2')
|
||||
|
||||
# Log in as staff, and check the problem is unanswered.
|
||||
self.login_staff()
|
||||
self.assertEqual(self.get_progress_detail(), u'0/2')
|
||||
|
||||
# Masquerade as the student, and check we can see the student state.
|
||||
self.update_masquerade(role='student', user_name=self.student_user.username)
|
||||
self.assertEqual(self.get_progress_detail(), u'2/2')
|
||||
|
||||
# Temporarily override the student state.
|
||||
self.submit_answer('Correct', 'Incorrect')
|
||||
self.assertEqual(self.get_progress_detail(), u'1/2')
|
||||
|
||||
# Reload the page and check we see the student state again.
|
||||
self.get_courseware_page()
|
||||
self.assertEqual(self.get_progress_detail(), u'2/2')
|
||||
|
||||
# Become the staff user again, and check the problem is still unanswered.
|
||||
self.update_masquerade(role='staff')
|
||||
self.assertEqual(self.get_progress_detail(), u'0/2')
|
||||
|
||||
# Verify the student state did not change.
|
||||
self.login_student()
|
||||
self.assertEqual(self.get_progress_detail(), u'2/2')
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestGetMasqueradingGroupId(StaffMasqueradeTestCase):
|
||||
"""
|
||||
@@ -252,3 +334,63 @@ class TestGetMasqueradingGroupId(StaffMasqueradeTestCase):
|
||||
group_id, user_partition_id = get_masquerading_group_info(self.test_user, self.course.id)
|
||||
self.assertEqual(group_id, 1)
|
||||
self.assertEqual(user_partition_id, 0)
|
||||
|
||||
|
||||
class ReadOnlyKeyValueStore(DictKeyValueStore):
|
||||
"""
|
||||
A KeyValueStore that raises an exception on attempts to modify it.
|
||||
|
||||
Used to make sure MasqueradingKeyValueStore does not try to modify the underlying KeyValueStore.
|
||||
"""
|
||||
def set(self, key, value):
|
||||
assert False, "ReadOnlyKeyValueStore may not be modified."
|
||||
|
||||
def delete(self, key):
|
||||
assert False, "ReadOnlyKeyValueStore may not be modified."
|
||||
|
||||
def set_many(self, update_dict): # pylint: disable=unused-argument
|
||||
assert False, "ReadOnlyKeyValueStore may not be modified."
|
||||
|
||||
|
||||
class FakeSession(dict):
|
||||
""" Mock for Django session object. """
|
||||
modified = False # We need dict semantics with a writable 'modified' property
|
||||
|
||||
|
||||
class MasqueradingKeyValueStoreTest(TestCase):
|
||||
"""
|
||||
Unit tests for the MasqueradingKeyValueStore class.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(MasqueradingKeyValueStoreTest, self).setUp()
|
||||
self.ro_kvs = ReadOnlyKeyValueStore({'a': 42, 'b': None, 'c': 'OpenCraft'})
|
||||
self.session = FakeSession()
|
||||
self.kvs = MasqueradingKeyValueStore(self.ro_kvs, self.session)
|
||||
|
||||
def test_all(self):
|
||||
self.assertEqual(self.kvs.get('a'), 42)
|
||||
self.assertEqual(self.kvs.get('b'), None)
|
||||
self.assertEqual(self.kvs.get('c'), 'OpenCraft')
|
||||
with self.assertRaises(KeyError):
|
||||
self.kvs.get('d')
|
||||
|
||||
self.assertTrue(self.kvs.has('a'))
|
||||
self.assertTrue(self.kvs.has('b'))
|
||||
self.assertTrue(self.kvs.has('c'))
|
||||
self.assertFalse(self.kvs.has('d'))
|
||||
|
||||
self.kvs.set_many({'a': 'Norwegian Blue', 'd': 'Giraffe'})
|
||||
self.kvs.set('b', 7)
|
||||
|
||||
self.assertEqual(self.kvs.get('a'), 'Norwegian Blue')
|
||||
self.assertEqual(self.kvs.get('b'), 7)
|
||||
self.assertEqual(self.kvs.get('c'), 'OpenCraft')
|
||||
self.assertEqual(self.kvs.get('d'), 'Giraffe')
|
||||
|
||||
for key in 'abd':
|
||||
self.assertTrue(self.kvs.has(key))
|
||||
self.kvs.delete(key)
|
||||
with self.assertRaises(KeyError):
|
||||
self.kvs.get(key)
|
||||
|
||||
self.assertEqual(self.kvs.get('c'), 'OpenCraft')
|
||||
|
||||
@@ -637,7 +637,7 @@ class TestTOC(ModuleStoreTestCase):
|
||||
course = self.store.get_course(self.toy_course.id, depth=2)
|
||||
with check_mongo_calls(toc_finds):
|
||||
actual = render.toc_for_course(
|
||||
self.request, course, self.chapter, None, self.field_data_cache
|
||||
self.request.user, self.request, course, self.chapter, None, self.field_data_cache
|
||||
)
|
||||
for toc_section in expected:
|
||||
self.assertIn(toc_section, actual)
|
||||
@@ -676,7 +676,7 @@ class TestTOC(ModuleStoreTestCase):
|
||||
|
||||
with check_mongo_calls(toc_finds):
|
||||
actual = render.toc_for_course(
|
||||
self.request, self.toy_course, self.chapter, section, self.field_data_cache
|
||||
self.request.user, self.request, self.toy_course, self.chapter, section, self.field_data_cache
|
||||
)
|
||||
for toc_section in expected:
|
||||
self.assertIn(toc_section, actual)
|
||||
@@ -1173,7 +1173,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
return render.get_module_for_descriptor_internal(
|
||||
user=self.user,
|
||||
descriptor=descriptor,
|
||||
field_data_cache=Mock(spec=FieldDataCache, name='field_data_cache'),
|
||||
student_data=Mock(spec=FieldData, name='student_data'),
|
||||
course_id=course_id,
|
||||
track_function=Mock(name='track_function'), # Track Function
|
||||
xqueue_callback_url_prefix=Mock(name='xqueue_callback_url_prefix'), # XQueue Callback Url Prefix
|
||||
@@ -1468,7 +1468,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
|
||||
"""
|
||||
super(LMSXBlockServiceBindingTest, self).setUp()
|
||||
self.user = UserFactory()
|
||||
self.field_data_cache = Mock()
|
||||
self.student_data = Mock()
|
||||
self.course = CourseFactory.create()
|
||||
self.track_function = Mock()
|
||||
self.xqueue_callback_url_prefix = Mock()
|
||||
@@ -1483,7 +1483,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
|
||||
descriptor = ItemFactory(category="pure", parent=self.course)
|
||||
runtime, _ = render.get_module_system_for_user(
|
||||
self.user,
|
||||
self.field_data_cache,
|
||||
self.student_data,
|
||||
descriptor,
|
||||
self.course.id,
|
||||
self.track_function,
|
||||
@@ -1502,7 +1502,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
|
||||
descriptor.days_early_for_beta = 5
|
||||
runtime, _ = render.get_module_system_for_user(
|
||||
self.user,
|
||||
self.field_data_cache,
|
||||
self.student_data,
|
||||
descriptor,
|
||||
self.course.id,
|
||||
self.track_function,
|
||||
|
||||
@@ -9,6 +9,7 @@ from textwrap import dedent
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
@@ -33,31 +34,10 @@ from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
|
||||
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
|
||||
|
||||
|
||||
class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
class ProblemSubmissionTestMixin(TestCase):
|
||||
"""
|
||||
Check that a course gets graded properly.
|
||||
TestCase mixin that provides functions to submit answers to problems.
|
||||
"""
|
||||
|
||||
# arbitrary constant
|
||||
COURSE_SLUG = "100"
|
||||
COURSE_NAME = "test_course"
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TestSubmittingProblems, self).setUp(create_user=False)
|
||||
# Create course
|
||||
self.course = CourseFactory.create(display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
|
||||
assert self.course, "Couldn't load course %r" % self.COURSE_NAME
|
||||
|
||||
# create a test student
|
||||
self.student = 'view@test.com'
|
||||
self.password = 'foo'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.enroll(self.course)
|
||||
self.student_user = User.objects.get(email=self.student)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def refresh_course(self):
|
||||
"""
|
||||
Re-fetch the course from the database so that the object being dealt with has everything added to it.
|
||||
@@ -68,7 +48,6 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Returns the url of the problem given the problem's name
|
||||
"""
|
||||
|
||||
return self.course.id.make_usage_key('problem', problem_url_name)
|
||||
|
||||
def modx_url(self, problem_location, dispatch):
|
||||
@@ -136,6 +115,32 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
resp = self.client.post(modx_url)
|
||||
return resp
|
||||
|
||||
|
||||
class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, ProblemSubmissionTestMixin):
|
||||
"""
|
||||
Check that a course gets graded properly.
|
||||
"""
|
||||
|
||||
# arbitrary constant
|
||||
COURSE_SLUG = "100"
|
||||
COURSE_NAME = "test_course"
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TestSubmittingProblems, self).setUp(create_user=False)
|
||||
# Create course
|
||||
self.course = CourseFactory.create(display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
|
||||
assert self.course, "Couldn't load course %r" % self.COURSE_NAME
|
||||
|
||||
# create a test student
|
||||
self.student = 'view@test.com'
|
||||
self.password = 'foo'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.enroll(self.course)
|
||||
self.student_user = User.objects.get(email=self.student)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def add_dropdown_to_section(self, section_location, name, num_inputs=2):
|
||||
"""
|
||||
Create and return a dropdown problem.
|
||||
|
||||
@@ -673,8 +673,8 @@ class TestAccordionDueDate(BaseDueDateTests):
|
||||
def get_text(self, course):
|
||||
""" Returns the HTML for the accordion """
|
||||
return views.render_accordion(
|
||||
self.request, course, course.get_children()[0].scope_ids.usage_id.to_deprecated_string(),
|
||||
None, None
|
||||
self.request.user, self.request, course,
|
||||
unicode(course.get_children()[0].scope_ids.usage_id), None, None
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ def courses(request):
|
||||
)
|
||||
|
||||
|
||||
def render_accordion(request, course, chapter, section, field_data_cache):
|
||||
def render_accordion(user, request, course, chapter, section, field_data_cache):
|
||||
"""
|
||||
Draws navigation bar. Takes current position in accordion as
|
||||
parameter.
|
||||
@@ -151,7 +151,7 @@ def render_accordion(request, course, chapter, section, field_data_cache):
|
||||
Returns the html string
|
||||
"""
|
||||
# grab the table of contents
|
||||
toc = toc_for_course(request, course, chapter, section, field_data_cache)
|
||||
toc = toc_for_course(user, request, course, chapter, section, field_data_cache)
|
||||
|
||||
context = dict([
|
||||
('toc', toc),
|
||||
@@ -378,10 +378,10 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
except ValueError:
|
||||
raise Http404(u"Position {} is not an integer!".format(position))
|
||||
|
||||
user = request.user
|
||||
course = get_course_with_access(user, 'load', course_key, depth=2)
|
||||
course = get_course_with_access(request.user, 'load', course_key, depth=2)
|
||||
staff_access = has_access(request.user, 'staff', course)
|
||||
masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
|
||||
|
||||
staff_access = has_access(user, 'staff', course)
|
||||
registered = registered_for_course(course, user)
|
||||
if not registered:
|
||||
# TODO (vshnayder): do course instructors need to be registered to see course?
|
||||
@@ -413,8 +413,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
if survey.utils.must_answer_survey(course, user):
|
||||
return redirect(reverse('course_survey', args=[unicode(course.id)]))
|
||||
|
||||
masquerade = setup_masquerade(request, course_key, staff_access)
|
||||
|
||||
try:
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course_key, user, course, depth=2)
|
||||
@@ -431,7 +429,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'accordion': render_accordion(request, course, chapter, section, field_data_cache),
|
||||
'accordion': render_accordion(user, request, course, chapter, section, field_data_cache),
|
||||
'COURSE_TITLE': course.display_name_with_default,
|
||||
'course': course,
|
||||
'init': '',
|
||||
@@ -475,7 +473,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
# settings.
|
||||
show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT']
|
||||
if show_chat:
|
||||
context['chat'] = chat_settings(course, user)
|
||||
context['chat'] = chat_settings(course, request.user)
|
||||
# If we couldn't load the chat settings, then don't show
|
||||
# the widget in the courseware.
|
||||
if context['chat'] is None:
|
||||
@@ -536,7 +534,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
)
|
||||
|
||||
section_module = get_module_for_descriptor(
|
||||
request.user,
|
||||
user,
|
||||
request,
|
||||
section_descriptor,
|
||||
field_data_cache,
|
||||
@@ -550,7 +548,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
# they don't have access to.
|
||||
raise Http404
|
||||
|
||||
# Save where we are in the chapter
|
||||
# Save where we are in the chapter.
|
||||
save_child_position(chapter_module, section)
|
||||
context['fragment'] = section_module.render(STUDENT_VIEW)
|
||||
context['section_title'] = section_descriptor.display_name_with_default
|
||||
@@ -598,12 +596,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
raise
|
||||
else:
|
||||
log.exception(
|
||||
u"Error in index view: user=%s, course=%s, chapter=%s, section=%s, position=%s",
|
||||
user,
|
||||
course,
|
||||
chapter,
|
||||
section,
|
||||
position
|
||||
u"Error in index view: user=%s, effective_user=%s, course=%s, chapter=%s section=%s position=%s",
|
||||
request.user, user, course, chapter, section, position
|
||||
)
|
||||
try:
|
||||
result = render_to_response('courseware/courseware-error.html', {
|
||||
@@ -683,19 +677,19 @@ def course_info(request, course_id):
|
||||
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
staff_access = has_access(request.user, 'staff', course)
|
||||
masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
|
||||
|
||||
# If the user needs to take an entrance exam to access this course, then we'll need
|
||||
# to send them to that specific course module before allowing them into other areas
|
||||
if user_must_complete_entrance_exam(request, request.user, course):
|
||||
if user_must_complete_entrance_exam(request, user, course):
|
||||
return redirect(reverse('courseware', args=[unicode(course.id)]))
|
||||
|
||||
# check to see if there is a required survey that must be taken before
|
||||
# the user can access the course.
|
||||
if request.user.is_authenticated() and survey.utils.must_answer_survey(course, request.user):
|
||||
if request.user.is_authenticated() and survey.utils.must_answer_survey(course, user):
|
||||
return redirect(reverse('course_survey', args=[unicode(course.id)]))
|
||||
|
||||
staff_access = has_access(request.user, 'staff', course)
|
||||
masquerade = setup_masquerade(request, course_key, staff_access) # allow staff to masquerade on the info page
|
||||
studio_url = get_studio_url(course, 'course_info')
|
||||
|
||||
# link to where the student should go to enroll in the course:
|
||||
@@ -704,7 +698,7 @@ def course_info(request, course_id):
|
||||
if settings.FEATURES.get('ENABLE_MKTG_SITE'):
|
||||
url_to_enroll = marketing_link('COURSES')
|
||||
|
||||
show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(request.user, course.id)
|
||||
show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(user, course.id)
|
||||
|
||||
context = {
|
||||
'request': request,
|
||||
@@ -719,7 +713,7 @@ def course_info(request, course_id):
|
||||
}
|
||||
|
||||
now = datetime.now(UTC())
|
||||
effective_start = _adjust_start_date_for_beta_testers(request.user, course, course_key)
|
||||
effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
|
||||
if not in_preview_mode() and staff_access and now < effective_start:
|
||||
# Disable student view button if user is staff and
|
||||
# course is not yet visible to students.
|
||||
|
||||
@@ -31,6 +31,7 @@ from shoppingcart.models import (
|
||||
|
||||
from track.views import task_track
|
||||
from util.file import course_filename_prefix_generator, UniversalNewlineIterator
|
||||
from xblock.runtime import KvsFieldData
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.split_test_module import get_split_user_partitions
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -43,7 +44,7 @@ from certificates.api import generate_user_certificates
|
||||
from courseware.courses import get_course_by_id, get_problems_in_section
|
||||
from courseware.grades import iterate_grades_for
|
||||
from courseware.models import StudentModule
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.model_data import DjangoKeyValueStore, FieldDataCache
|
||||
from courseware.module_render import get_module_for_descriptor_internal
|
||||
from instructor_analytics.basic import enrolled_students_features, list_may_enroll
|
||||
from instructor_analytics.csvs import format_dictlist
|
||||
@@ -422,6 +423,7 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule
|
||||
"""
|
||||
# reconstitute the problem's corresponding XModule:
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(course_id, student, module_descriptor)
|
||||
student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache))
|
||||
|
||||
# get request-related tracking information from args passthrough, and supplement with task-specific
|
||||
# information:
|
||||
@@ -444,7 +446,7 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule
|
||||
return get_module_for_descriptor_internal(
|
||||
user=student,
|
||||
descriptor=module_descriptor,
|
||||
field_data_cache=field_data_cache,
|
||||
student_data=student_data,
|
||||
course_id=course_id,
|
||||
track_function=make_track_function(),
|
||||
xqueue_callback_url_prefix=xqueue_callback_url_prefix,
|
||||
|
||||
@@ -22,6 +22,24 @@
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.action-preview-select {
|
||||
margin-right: $baseline;
|
||||
}
|
||||
|
||||
.action-preview-username-container {
|
||||
display: none;
|
||||
|
||||
.action-preview-username {
|
||||
vertical-align: middle;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-specific-student-notice {
|
||||
margin-top: ($baseline/2);
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,17 @@ def url_class(is_active):
|
||||
if is_active:
|
||||
return "active"
|
||||
return ""
|
||||
%>
|
||||
<%
|
||||
cohorted_user_partition = get_cohorted_user_partition(course.id)
|
||||
show_preview_menu = not disable_preview_menu and staff_access and active_page in ['courseware', 'info']
|
||||
is_student_masquerade = masquerade and masquerade.role == 'student'
|
||||
masquerade_group_id = masquerade.group_id if masquerade else None
|
||||
|
||||
def selected(is_selected):
|
||||
return "selected" if is_selected else ""
|
||||
|
||||
show_preview_menu = not disable_preview_menu and staff_access and active_page in ["courseware", "info"]
|
||||
cohorted_user_partition = get_cohorted_user_partition(course.id)
|
||||
masquerade_user_name = masquerade.user_name if masquerade else None
|
||||
masquerade_group_id = masquerade.group_id if masquerade else None
|
||||
staff_selected = selected(not masquerade or masquerade.role != "student")
|
||||
specific_student_selected = selected(not staff_selected and masquerade.user_name)
|
||||
student_selected = selected(not staff_selected and not specific_student_selected and not masquerade_group_id)
|
||||
%>
|
||||
|
||||
% if show_preview_menu:
|
||||
@@ -34,20 +39,32 @@ def url_class(is_active):
|
||||
<form action="#" class="action-preview-form" method="post">
|
||||
<label for="action-preview-select" class="action-preview-label">${_("View this course as:")}</label>
|
||||
<select class="action-preview-select" id="action-preview-select" name="select">
|
||||
<option value="staff" ${"selected" if not is_student_masquerade else ""}>${_("Staff")}</option>
|
||||
<option value="student" ${"selected" if is_student_masquerade and not masquerade_group_id else ""}>${_("Student")}</option>
|
||||
<option value="staff" ${staff_selected}>${_("Staff")}</option>
|
||||
<option value="student" ${student_selected}>${_("Student")}</option>
|
||||
<option value="specific student" ${specific_student_selected}>${_("Specific student")}</option>
|
||||
% if cohorted_user_partition:
|
||||
% for group in sorted(cohorted_user_partition.groups, key=lambda group: group.name):
|
||||
<option value="group.id" data-group-id="${group.id}" ${"selected" if masquerade_group_id == group.id else ""}>
|
||||
<option value="group.id" data-group-id="${group.id}" ${selected(masquerade_group_id == group.id)}>
|
||||
${_("Student in {content_group}").format(content_group=group.name)}
|
||||
</option>
|
||||
% endfor
|
||||
% endif
|
||||
</select>
|
||||
<button type="submit" class="sr" name="submit" value="submit">${_("set preview mode")}</button>
|
||||
<div class="action-preview-username-container">
|
||||
<label for="action-preview-username" class="action-preview-label">${_("Username or email:")}</label>
|
||||
<input type="text" class="action-preview-username" id="action-preview-username">
|
||||
</div>
|
||||
<button type="submit" class="sr" name="submit" value="submit">${_("Set preview mode")}</button>
|
||||
</form>
|
||||
</li>
|
||||
</ol>
|
||||
% if specific_student_selected:
|
||||
<div class="preview-specific-student-notice">
|
||||
<p>
|
||||
${_("You are now viewing the course as <i>{user_name}</i>.").format(user_name=masquerade_user_name)}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</nav>
|
||||
% endif
|
||||
@@ -84,22 +101,55 @@ def url_class(is_active):
|
||||
% if show_preview_menu:
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var element = $('.action-preview-select');
|
||||
var selectElement = $('.action-preview-select');
|
||||
var userNameElement = $('#action-preview-username');
|
||||
var userNameContainer = $('.action-preview-username-container')
|
||||
|
||||
% if disable_student_access:
|
||||
element.attr("disabled", true);
|
||||
element.attr("title", "${_("Course is not yet visible to students.")}");
|
||||
selectElement.attr("disabled", true);
|
||||
selectElement.attr("title", "${_("Course is not yet visible to students.")}");
|
||||
% endif
|
||||
|
||||
element.change(function() {
|
||||
var selectedOption, data;
|
||||
if (element.attr("disabled")) {
|
||||
% if specific_student_selected:
|
||||
userNameContainer.css('display', 'inline-block');
|
||||
userNameElement.val('${masquerade_user_name}');
|
||||
% endif
|
||||
|
||||
selectElement.change(function() {
|
||||
var selectedOption;
|
||||
if (selectElement.attr("disabled")) {
|
||||
return alert("${_("You cannot view the course as a student or beta tester before the course release date.")}");
|
||||
}
|
||||
selectedOption = element.find('option:selected');
|
||||
data = {
|
||||
selectedOption = selectElement.find('option:selected');
|
||||
if (selectedOption.val() === 'specific student') {
|
||||
userNameContainer.css('display', 'inline-block');
|
||||
} else {
|
||||
userNameContainer.hide();
|
||||
masquerade(selectedOption);
|
||||
}
|
||||
});
|
||||
|
||||
userNameElement.keypress(function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
// Avoid submitting the form on enter, since the submit action isn't implemented. Instead, blur the
|
||||
// element to trigger a change event in case the value was edited, which in turn will trigger an AJAX
|
||||
// request to update the masquerading data.
|
||||
userNameElement.blur();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
userNameElement.change(function() {
|
||||
masquerade(selectElement.find('option:selected'));
|
||||
});
|
||||
|
||||
function masquerade(selectedOption) {
|
||||
var data = {
|
||||
role: selectedOption.val() === 'staff' ? 'staff' : 'student',
|
||||
user_partition_id: ${cohorted_user_partition.id if cohorted_user_partition else 'null'},
|
||||
group_id: selectedOption.data('group-id')
|
||||
group_id: selectedOption.data('group-id'),
|
||||
user_name: selectedOption.val() === 'specific student' ? userNameElement.val() : null
|
||||
};
|
||||
$.ajax({
|
||||
url: '/courses/${course.id}/masquerade',
|
||||
@@ -108,13 +158,17 @@ def url_class(is_active):
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function(result) {
|
||||
location.reload();
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: cannot connect to server');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
% endif
|
||||
|
||||
Reference in New Issue
Block a user