def get_module_system_for_user(user, field_data_cache, # Arguments preceding this comment have user binding, those following don't descriptor, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path='', user_location=None): """ Helper function that returns a module system and student_data bound to a user and a descriptor. The purpose of this function is to factor out everywhere a user is implicitly bound when creating a module, to allow an existing module to be re-bound to a user. Most of the user bindings happen when creating the 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 are all the other arguments. Ultimately, this isn't too different than how get_module_for_descriptor_internal was before refactoring. Arguments: see arguments for get_module() 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'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict( course_id=course_id.to_deprecated_string(), userid=str(user.id), mod_id=descriptor.location.to_deprecated_string(), dispatch=dispatch ), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': XQUEUE_INTERFACE, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path, user_location) def handle_grade_event(block, event_type, event): user_id = event.get('user_id', user.id) # Construct the key for the module key = KeyValueStore.Key( scope=Scope.user_state, user_id=user_id, block_scope_id=descriptor.location, field_name='grade' ) student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) tags = [ u"org:{}".format(course_id.org), u"course:{}".format(course_id), u"score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) dog_stats_api.increment("lms.courseware.question_answered", tags=tags) def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == 'grade': handle_grade_event(block, event_type, event) else: track_function(event_type, event) def rebind_noauth_module_to_user(module, real_user): """ A function that allows a module to get re-bound to a real user if it was previously bound to an AnonymousUser. Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler. Arguments: module (any xblock type): the module to rebind real_user (django.contrib.auth.models.User): the user to bind to Returns: nothing (but the side effect is that module is re-bound to real_user) """ if user.is_authenticated(): err_msg = ("rebind_noauth_module_to_user can only be called from a module bound to " "an anonymous user") log.error(err_msg) raise LmsModuleRenderError(err_msg) field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents( course_id, real_user, module.descriptor ) (inner_system, inner_student_data) = get_module_system_for_user( real_user, field_data_cache_real_user, # These have implicit user bindings, rest of args considered not to module.descriptor, course_id, track_function, xqueue_callback_url_prefix, position, wrap_xmodule_display, grade_bucket_type, static_asset_path, user_location ) # rebinds module to a different student. We'll change system, student_data, and scope_ids module.descriptor.bind_for_student( inner_system, LmsFieldData(module.descriptor._field_data, inner_student_data) # pylint: disable=protected-access ) module.descriptor.scope_ids = ( module.descriptor.scope_ids._replace(user_id=real_user.id) # pylint: disable=protected-access ) module.scope_ids = module.descriptor.scope_ids # this is needed b/c NamedTuples are immutable # now bind the module to the new ModuleSystem instance and vice-versa module.runtime = inner_system inner_system.xmodule_instance = module # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append(partial( wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()) )) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append(partial( replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path )) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append(partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''}), )) 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)) # 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 # the per-student anonymized id (as we have in the past), # while giving selected modules a per-course anonymized id. # As we have the time to manually test more modules, we can add to the list # of modules that get the per-course anonymized id. is_pure_xblock = isinstance(descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) module_class = getattr(descriptor, 'module_class', None) is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule) if is_pure_xblock or is_lti_module: anonymous_student_id = anonymous_id_for_user(user, course_id) else: anonymous_student_id = anonymous_id_for_user(user, None) system = LmsModuleSystem( track_function=track_function, render_template=render_to_string, static_url=settings.STATIC_URL, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, course_key=course_id ), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''}) ), node_path=settings.NODE_PATH, publish=publish, anonymous_student_id=anonymous_student_id, course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ 'i18n': ModuleI18nService(), }, get_user_role=lambda: get_user_role(user, course_id), descriptor_runtime=descriptor.runtime, rebind_noauth_module_to_user=rebind_noauth_module_to_user, user_location=user_location, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location) ) system.set(u'user_is_staff', has_access(user, u'staff', descriptor.location, course_id)) system.set(u'user_is_admin', has_access(user, u'staff', 'global')) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, u'staff', descriptor.location, course_id): system.error_descriptor_class = ErrorDescriptor else: system.error_descriptor_class = NonStaffErrorDescriptor return system, student_data
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path=''): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache)) descriptor._field_data = LmsFieldData(descriptor._field_data, student_data) def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path) def publish(event, custom_user=None): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get('event_name') != 'grade': return if custom_user: user_id = custom_user.id else: user_id = user.id # Construct the key for the module key = KeyValueStore.Key(scope=Scope.user_state, user_id=user_id, block_scope_id=descriptor.location, field_name='grade') student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) dog_stats_api.increment("lms.courseware.question_answered", tags=tags) # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append( partial(wrap_xblock, partial(handler_prefix, course_id))) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append( partial(replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append( partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' }), )) if settings.FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, descriptor, 'staff', course_id): block_wrappers.append(partial(add_histogram, user)) # 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 # the per-student anonymized id (as we have in the past), # while giving selected modules a per-course anonymized id. # As we have the time to manually test more modules, we can add to the list # of modules that get the per-course anonymized id. is_pure_xblock = isinstance( descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) is_lti_module = not is_pure_xblock and issubclass(descriptor.module_class, LTIModule) if is_pure_xblock or is_lti_module: anonymous_student_id = anonymous_id_for_user(user, course_id) else: anonymous_student_id = anonymous_id_for_user(user, '') system = LmsModuleSystem( track_function=track_function, render_template=render_to_string, static_url=settings.STATIC_URL, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_id=course_id), replace_jump_to_id_urls=partial(static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse( 'jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' })), node_path=settings.NODE_PATH, publish=publish, anonymous_student_id=anonymous_student_id, course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ # django.utils.translation implements the gettext.Translations # interface (it has ugettext, ungettext, etc), so we can use it # directly as the runtime i18n service. 'i18n': django.utils.translation, }, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): system.error_descriptor_class = ErrorDescriptor else: system.error_descriptor_class = NonStaffErrorDescriptor descriptor.xmodule_runtime = system descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) return descriptor
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path=''): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Do not check access when it's a noauth request. if getattr(user, 'known', True): # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache)) descriptor._field_data = LmsFieldData(descriptor._field_data, student_data) def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict( course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch ), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path) def publish(block, event, custom_user=None): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get('event_name') != 'grade': return if custom_user: user_id = custom_user.id else: user_id = user.id # Construct the key for the module key = KeyValueStore.Key( scope=Scope.user_state, user_id=user_id, block_scope_id=descriptor.location, field_name='grade' ) student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ u"org:{0}".format(org), u"course:{0}".format(course_num), u"run:{0}".format(run), u"score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) dog_stats_api.increment("lms.courseware.question_answered", tags=tags) # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append(partial(wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id})) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append(partial( replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path )) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append(partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''}), )) if settings.FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, descriptor, 'staff', course_id): block_wrappers.append(partial(add_histogram, user)) # 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 # the per-student anonymized id (as we have in the past), # while giving selected modules a per-course anonymized id. # As we have the time to manually test more modules, we can add to the list # of modules that get the per-course anonymized id. is_pure_xblock = isinstance(descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) module_class = getattr(descriptor, 'module_class', None) is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule) if is_pure_xblock or is_lti_module: anonymous_student_id = anonymous_id_for_user(user, course_id) else: anonymous_student_id = anonymous_id_for_user(user, '') system = LmsModuleSystem( track_function=track_function, render_template=render_to_string, static_url=settings.STATIC_URL, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, course_id=course_id ), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''}) ), node_path=settings.NODE_PATH, publish=publish, anonymous_student_id=anonymous_student_id, course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ # django.utils.translation implements the gettext.Translations # interface (it has ugettext, ungettext, etc), so we can use it # directly as the runtime i18n service. 'i18n': django.utils.translation, }, get_user_role=lambda: get_user_role(user, course_id), ) # pass position specified in URL to module through ModuleSystem system.set('position', position) if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url()) ) system.set(u'user_is_staff', has_access(user, descriptor.location, u'staff', course_id)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): system.error_descriptor_class = ErrorDescriptor else: system.error_descriptor_class = NonStaffErrorDescriptor descriptor.xmodule_runtime = system descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) return descriptor
def get_module_system_for_user( user, field_data_cache, # Arguments preceding this comment have user binding, those following don't descriptor, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path='', user_location=None): """ Helper function that returns a module system and student_data bound to a user and a descriptor. The purpose of this function is to factor out everywhere a user is implicitly bound when creating a module, to allow an existing module to be re-bound to a user. Most of the user bindings happen when creating the 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 are all the other arguments. Ultimately, this isn't too different than how get_module_for_descriptor_internal was before refactoring. Arguments: see arguments for get_module() 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'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=course_id.to_deprecated_string(), userid=str(user.id), mod_id=descriptor.location.to_deprecated_string(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': XQUEUE_INTERFACE, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path, user_location) def handle_grade_event(block, event_type, event): user_id = event.get('user_id', user.id) # Construct the key for the module key = KeyValueStore.Key(scope=Scope.user_state, user_id=user_id, block_scope_id=descriptor.location, field_name='grade') student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) tags = [ u"org:{}".format(course_id.org), u"course:{}".format(course_id), u"score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) dog_stats_api.increment("lms.courseware.question_answered", tags=tags) def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == 'grade': handle_grade_event(block, event_type, event) else: track_function(event_type, event) def rebind_noauth_module_to_user(module, real_user): """ A function that allows a module to get re-bound to a real user if it was previously bound to an AnonymousUser. Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler. Arguments: module (any xblock type): the module to rebind real_user (django.contrib.auth.models.User): the user to bind to Returns: nothing (but the side effect is that module is re-bound to real_user) """ if user.is_authenticated(): err_msg = ( "rebind_noauth_module_to_user can only be called from a module bound to " "an anonymous user") log.error(err_msg) raise LmsModuleRenderError(err_msg) field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents( course_id, real_user, module.descriptor) (inner_system, inner_student_data) = get_module_system_for_user( real_user, field_data_cache_real_user, # These have implicit user bindings, rest of args considered not to module.descriptor, course_id, track_function, xqueue_callback_url_prefix, position, wrap_xmodule_display, grade_bucket_type, static_asset_path, user_location) # rebinds module to a different student. We'll change system, student_data, and scope_ids module.descriptor.bind_for_student( inner_system, LmsFieldData(module.descriptor._field_data, inner_student_data) # pylint: disable=protected-access ) module.descriptor.scope_ids = ( module.descriptor.scope_ids._replace(user_id=real_user.id) # pylint: disable=protected-access ) module.scope_ids = module.descriptor.scope_ids # this is needed b/c NamedTuples are immutable # now bind the module to the new ModuleSystem instance and vice-versa module.runtime = inner_system inner_system.xmodule_instance = module # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append( partial(wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes( usage_id.to_deprecated_string()))) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append( partial(replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append( partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={ 'course_id': course_id.to_deprecated_string(), 'module_id': '' }), )) 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)) # 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 # the per-student anonymized id (as we have in the past), # while giving selected modules a per-course anonymized id. # As we have the time to manually test more modules, we can add to the list # of modules that get the per-course anonymized id. is_pure_xblock = isinstance( descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) module_class = getattr(descriptor, 'module_class', None) is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule) if is_pure_xblock or is_lti_module: anonymous_student_id = anonymous_id_for_user(user, course_id) else: anonymous_student_id = anonymous_id_for_user(user, None) system = LmsModuleSystem( track_function=track_function, render_template=render_to_string, static_url=settings.STATIC_URL, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_key=course_id), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse('jump_to_id', kwargs={ 'course_id': course_id.to_deprecated_string(), 'module_id': '' })), node_path=settings.NODE_PATH, publish=publish, anonymous_student_id=anonymous_student_id, course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ 'i18n': ModuleI18nService(), }, get_user_role=lambda: get_user_role(user, course_id), descriptor_runtime=descriptor.runtime, rebind_noauth_module_to_user=rebind_noauth_module_to_user, user_location=user_location, ) # pass position specified in URL to module through ModuleSystem if position is not None: try: position = int(position) except (ValueError, TypeError): log.exception('Non-integer %r passed as position.', position) position = None system.set('position', position) if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location)) system.set(u'user_is_staff', has_access(user, u'staff', descriptor.location, course_id)) system.set(u'user_is_admin', has_access(user, u'staff', 'global')) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, u'staff', descriptor.location, course_id): system.error_descriptor_class = ErrorDescriptor else: system.error_descriptor_class = NonStaffErrorDescriptor return system, student_data