def _section_send_email(course_key, access, course): """ Provide data for the corresponding bulk email section """ html_module = HtmlDescriptor( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) ) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_key.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()) ) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': course_key.to_deprecated_string()}), 'editor': email_editor, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': course_key.to_deprecated_string()} ), } return section_data
def submit_student_answer(self, username, problem_url_name, responses): """ Use ajax interface to submit a student answer. Assumes the input list of responses has two values. """ def get_input_id(response_id): """Creates input id using information about the test course and the current problem.""" # Note that this is a capa-specific convention. The form is a version of the problem's # URL, modified so that it can be easily stored in html, prepended with "input-" and # appended with a sequence identifier for the particular response the input goes to. return 'input_i4x-{0}-{1}-problem-{2}_{3}'.format(TEST_COURSE_ORG.lower(), TEST_COURSE_NUMBER.replace('.', '_'), problem_url_name, response_id) # make sure that the requested user is logged in, so that the ajax call works # on the right problem: self.login_username(username) # make ajax call: modx_url = reverse('xblock_handler', kwargs={ 'course_id': self.course.id.to_deprecated_string(), 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()), 'handler': 'xmodule_handler', 'suffix': 'problem_check', }) # we assume we have two responses, so assign them the correct identifiers. resp = self.client.post(modx_url, { get_input_id('2_1'): responses[0], get_input_id('3_1'): responses[1], }) return resp
def handle_callback_and_get_display_name_from_event(self, mock_tracker, problem_display_name=None): """ Creates a fake module, invokes the callback and extracts the display name from the emitted problem_check event. """ descriptor_kwargs = { 'category': 'problem', 'data': self.problem_xml } if problem_display_name: descriptor_kwargs['display_name'] = problem_display_name descriptor = ItemFactory.create(**descriptor_kwargs) render.handle_xblock_callback( self.request, self.course.id, quote_slashes(str(descriptor.location)), 'xmodule_handler', 'problem_check', ) self.assertEquals(len(mock_tracker.send.mock_calls), 1) mock_call = mock_tracker.send.mock_calls[0] event = mock_call[1][0] self.assertEquals(event['event_type'], 'problem_check') return event['context']['module']['display_name']
def _section_send_email(course_key, access, course): """ Provide data for the corresponding bulk email section """ # This HtmlDescriptor is only being used to generate a nice text editor. html_module = HtmlDescriptor( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) ) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_key.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()), # Generate a new request_token here at random, because this module isn't connected to any other # xblock rendering. request_token=uuid.uuid1().get_hex() ) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': course_key.to_deprecated_string()}), 'editor': email_editor, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': course_key.to_deprecated_string()} ), } return section_data
def expected_handler_url(self, handler): """convenience method to get the reversed handler urls""" return "https://{}{}".format(settings.SITE_NAME, reverse( 'courseware.module_render.handle_xblock_callback_noauth', args=[ self.course.id.to_deprecated_string(), quote_slashes(unicode(self.lti_published.scope_ids.usage_id.to_deprecated_string()).encode('utf-8')), handler ] ))
def test_xmodule_dispatch(self): request = self.request_factory.post("dummy_url", data={"position": 1}) request.user = self.mock_user response = render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), "xmodule_handler", "goto_position", ) self.assertIsInstance(response, HttpResponse)
def test_bad_location(self): request = self.request_factory.post("dummy_url") request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key("chapter", "bad_location").to_deprecated_string()), "xmodule_handler", "goto_position", )
def test_bad_course_id(self): request = self.request_factory.post("dummy_url") request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, "bad_course_id", quote_slashes(self.location.to_deprecated_string()), "xmodule_handler", "goto_position", )
def test_missing_handler(self): request = self.request_factory.post("dummy_url") request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), "bad_handler", "bad_dispatch", )
def test_missing_handler(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_id, quote_slashes(str(self.location)), 'bad_handler', 'bad_dispatch', )
def test_bad_xmodule_dispatch(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'bad_dispatch', )
def test_bad_location(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_id, quote_slashes(str(Location('i4x', 'edX', 'toy', 'chapter', 'bad_location'))), 'xmodule_handler', 'goto_position', )
def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=False, extra_data=None): # pylint: disable=unused-argument """ Wraps the results of rendering an XBlock view in a standard <section> with identifying data so that the appropriate javascript module can be loaded onto it. :param runtime_class: The name of the javascript runtime class to use to load this block :param block: An XBlock (that may be an XModule or XModuleDescriptor) :param view: The name of the view that rendered the fragment being wrapped :param frag: The :class:`Fragment` to be wrapped :param context: The context passed to the view being rendered :param display_name_only: If true, don't render the fragment content at all. Instead, just render the `display_name` of `block` :param extra_data: A dictionary with extra data values to be set on the wrapper """ if extra_data is None: extra_data = {} # If any mixins have been applied, then use the unmixed class class_name = getattr(block, 'unmixed_class', block.__class__).__name__ data = {} data.update(extra_data) css_classes = ['xblock', 'xblock-' + view] if isinstance(block, (XModule, XModuleDescriptor)): if view == 'student_view': # The block is acting as an XModule css_classes.append('xmodule_display') elif view == 'mobi_student_view': # The block is acting as an XModule css_classes.append('xmodule_display') elif view == 'studio_view': # The block is acting as an XModuleDescriptor css_classes.append('xmodule_edit') css_classes.append('xmodule_' + class_name) data['type'] = block.js_module_name shim_xmodule_js(frag) if frag.js_init_fn: data['init'] = frag.js_init_fn data['runtime-class'] = runtime_class data['runtime-version'] = frag.js_init_version data['block-type'] = block.scope_ids.block_type data['usage-id'] = quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')) template_context = { 'content': block.display_name if display_name_only else frag.content, 'classes': css_classes, 'display_name': block.display_name_with_default, 'data_attributes': ' '.join(u'data-{}="{}"'.format(key, value) for key, value in data.items()), } return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context))
def test_xmodule_dispatch(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user response = render.handle_xblock_callback( request, self.course_id, quote_slashes(str(self.location)), 'xmodule_handler', 'goto_position', ) self.assertIsInstance(response, HttpResponse)
def test_bad_course_id(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, 'bad_course_id', quote_slashes(str(self.location)), 'xmodule_handler', 'goto_position', )
def handler_prefix(block, handler='', suffix=''): """ Return a url prefix for XBlock handler_url. The full handler_url should be '{prefix}/{handler}/{suffix}?{query}'. Trailing `/`s are removed from the returned url. """ return reverse('preview_handler', kwargs={ 'usage_id': quote_slashes(str(block.scope_ids.usage_id)), 'handler': handler, 'suffix': suffix, }).rstrip('/?')
def test_anonymous_handle_xblock_callback(self): dispatch_url = reverse( 'xblock_handler', args=[ self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key('videosequence', 'Toy_Videos').to_deprecated_string()), 'xmodule_handler', 'goto_position' ] ) response = self.client.post(dispatch_url, {'position': 2}) self.assertEquals(403, response.status_code)
def test_anonymous_handle_xblock_callback(self): dispatch_url = reverse( 'xblock_handler', args=[ 'edX/toy/2012_Fall', quote_slashes('i4x://edX/toy/videosequence/Toy_Videos'), 'xmodule_handler', 'goto_position' ] ) response = self.client.post(dispatch_url, {'position': 2}) self.assertEquals(403, response.status_code)
def test_anonymous_handle_xblock_callback(self): dispatch_url = reverse( "xblock_handler", args=[ self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key("videosequence", "Toy_Videos").to_deprecated_string()), "xmodule_handler", "goto_position", ], ) response = self.client.post(dispatch_url, {"position": 2}) self.assertEquals(403, response.status_code) self.assertEquals("Unauthenticated", response.content)
def get_problem(self): pun = 'H1P1' problem_location = "i4x://edX/graded/problem/%s" % pun modx_url = reverse('xblock_handler', kwargs={'course_id': self.graded_course.id, 'usage_id': quote_slashes(problem_location), 'handler': 'xmodule_handler', 'suffix': 'problem_get'}) resp = self.client.get(modx_url) print "modx_url ", modx_url return resp
def get_problem(self): pun = 'H1P1' problem_location = self.graded_course.id.make_usage_key("problem", pun) modx_url = reverse('xblock_handler', kwargs={'course_id': self.graded_course.id.to_deprecated_string(), 'usage_id': quote_slashes(problem_location.to_deprecated_string()), 'handler': 'xmodule_handler', 'suffix': 'problem_get'}) resp = self.client.get(modx_url) print "modx_url ", modx_url return resp
def render_problem(self, username, problem_url_name): """ Use ajax interface to request html for a problem. """ # make sure that the requested user is logged in, so that the ajax call works # on the right problem: self.login_username(username) # make ajax call: modx_url = reverse('xblock_handler', kwargs={'course_id': self.course.id, 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name)), 'handler': 'xmodule_handler', 'suffix': 'problem_get', }) resp = self.client.post(modx_url, {}) return resp
def test_too_large_file(self): inputfile = self._mock_file(size=1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE) request = self.request_factory.post("dummy_url", data={"file_id": inputfile}) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), "dummy_handler", ).content, json.dumps( { "success": 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) } ), )
def get_problem(self): pun = "H1P1" problem_location = "i4x://edX/graded/problem/%s" % pun modx_url = reverse( "xblock_handler", kwargs={ "course_id": self.graded_course.id, "usage_id": quote_slashes(problem_location), "handler": "xmodule_handler", "suffix": "problem_get", }, ) resp = self.client.get(modx_url) print "modx_url ", modx_url return resp
def modx_url(self, problem_location, dispatch): """ Return the url needed for the desired action. problem_location: location of the problem on which we want some action dispatch: the the action string that gets passed to the view as a kwarg example: 'check_problem' for having responses processed """ return reverse( 'xblock_handler', kwargs={ 'course_id': self.course.id, 'usage_id': quote_slashes(problem_location), 'handler': 'xmodule_handler', 'suffix': dispatch, } )
def test_too_many_files(self): request = self.request_factory.post( 'dummy_url', data={'file_id': (self._mock_file(), ) * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, 'dummy/course/id', quote_slashes(str(self.location)), 'dummy_handler' ).content, json.dumps({ 'success': 'Submission aborted! Maximum %d files may be submitted at once' % settings.MAX_FILEUPLOADS_PER_INPUT }) )
def handler_url(block, handler_name, suffix='', query='', thirdparty=False): """ Handler URL function for Studio """ if thirdparty: raise NotImplementedError("edX Studio doesn't support third-party xblock handler urls") url = reverse('component_handler', kwargs={ 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')), 'handler': handler_name, 'suffix': suffix, }).rstrip('/') if query: url += '?' + query return url
def test_too_many_files(self): request = self.request_factory.post( "dummy_url", data={"file_id": (self._mock_file(),) * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), "dummy_handler", ).content, json.dumps( { "success": "Submission aborted! Maximum %d files may be submitted at once" % settings.MAX_FILEUPLOADS_PER_INPUT } ), )
def test_too_large_file(self): inputfile = self._mock_file(size=1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE) request = self.request_factory.post( 'dummy_url', data={'file_id': inputfile} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, 'dummy/course/id', quote_slashes(str(self.location)), 'dummy_handler' ).content, json.dumps({ 'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) }) )
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # This HtmlDescriptor is only being used to generate a nice text editor. html_module = HtmlDescriptor( course.system, DictFieldData({"data": ""}), ScopeIds(None, None, None, course_key.make_usage_key("html", "fake")), ) fragment = course.system.render(html_module, "studio_view") fragment = wrap_xblock( "LmsRuntime", html_module, "studio_view", fragment, None, extra_data={"course-id": course_key.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()), # Generate a new request_token here at random, because this module isn't connected to any other # xblock rendering. request_token=uuid.uuid1().get_hex(), ) email_editor = fragment.content section_data = { "section_key": "send_email", "section_display_name": _("Email"), "access": access, "send_email": reverse("send_email", kwargs={"course_id": course_key.to_deprecated_string()}), "editor": email_editor, "list_instructor_tasks_url": reverse( "list_instructor_tasks", kwargs={"course_id": course_key.to_deprecated_string()} ), "email_background_tasks_url": reverse( "list_background_email_tasks", kwargs={"course_id": course_key.to_deprecated_string()} ), "email_content_history_url": reverse( "list_email_content", kwargs={"course_id": course_key.to_deprecated_string()} ), } return section_data
def submit_student_answer(self, username, problem_url_name, responses): """ Use ajax interface to submit a student answer. Assumes the input list of responses has two values. """ def get_input_id(response_id): """Creates input id using information about the test course and the current problem.""" # Note that this is a capa-specific convention. The form is a version of the problem's # URL, modified so that it can be easily stored in html, prepended with "input-" and # appended with a sequence identifier for the particular response the input goes to. return 'input_i4x-{0}-{1}-problem-{2}_{3}'.format( TEST_COURSE_ORG.lower(), TEST_COURSE_NUMBER.replace('.', '_'), problem_url_name, response_id) # make sure that the requested user is logged in, so that the ajax call works # on the right problem: self.login_username(username) # make ajax call: modx_url = reverse( 'xblock_handler', kwargs={ 'course_id': self.course.id, 'usage_id': quote_slashes( InstructorTaskModuleTestCase.problem_location( problem_url_name)), 'handler': 'xmodule_handler', 'suffix': 'problem_check', }) # we assume we have two responses, so assign them the correct identifiers. resp = self.client.post(modx_url, { get_input_id('2_1'): responses[0], get_input_id('3_1'): responses[1], }) return resp
def _section_send_email(course_key, access, course): """ Provide data for the corresponding bulk email section """ html_module = HtmlDescriptor( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_key.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes( usage_id.to_deprecated_string())) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': course_key.to_deprecated_string()}), 'editor': email_editor, 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}), 'email_background_tasks_url': reverse('list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()}), 'email_content_history_url': reverse('list_email_content', kwargs={'course_id': course_key.to_deprecated_string()}), } return section_data
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # This HtmlDescriptor is only being used to generate a nice text editor. html_module = HtmlDescriptor( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) ) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_key.to_deprecated_string()}, usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()), # Generate a new request_token here at random, because this module isn't connected to any other # xblock rendering. request_token=uuid.uuid1().get_hex() ) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': course_key.to_deprecated_string()}), 'editor': email_editor, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': course_key.to_deprecated_string()} ), } return section_data
def render_problem(self, username, problem_url_name): """ Use ajax interface to request html for a problem. """ # make sure that the requested user is logged in, so that the ajax call works # on the right problem: self.login_username(username) # make ajax call: modx_url = reverse( 'xblock_handler', kwargs={ 'course_id': self.course.id, 'usage_id': quote_slashes( InstructorTaskModuleTestCase.problem_location( problem_url_name)), 'handler': 'xmodule_handler', 'suffix': 'problem_get', }) resp = self.client.post(modx_url, {}) return resp
def handler_url(block, handler_name, suffix='', query='', thirdparty=False): """ Handler URL function for Studio """ if thirdparty: raise NotImplementedError( "edX Studio doesn't support third-party xblock handler urls") url = reverse('component_handler', kwargs={ 'usage_id': quote_slashes( unicode(block.scope_ids.usage_id).encode('utf-8')), 'handler': handler_name, 'suffix': suffix, }).rstrip('/') if query: url += '?' + query return url
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): return reverse('preview_handler', kwargs={ 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')), 'handler': handler_name, 'suffix': suffix, }) + '?' + query
def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=False, extra_data=None): # pylint: disable=unused-argument """ Wraps the results of rendering an XBlock view in a standard <section> with identifying data so that the appropriate javascript module can be loaded onto it. :param runtime_class: The name of the javascript runtime class to use to load this block :param block: An XBlock (that may be an XModule or XModuleDescriptor) :param view: The name of the view that rendered the fragment being wrapped :param frag: The :class:`Fragment` to be wrapped :param context: The context passed to the view being rendered :param display_name_only: If true, don't render the fragment content at all. Instead, just render the `display_name` of `block` :param extra_data: A dictionary with extra data values to be set on the wrapper """ if extra_data is None: extra_data = {} # If any mixins have been applied, then use the unmixed class class_name = getattr(block, 'unmixed_class', block.__class__).__name__ data = {} data.update(extra_data) css_classes = ['xblock', 'xblock-' + view] if isinstance(block, (XModule, XModuleDescriptor)): if view == 'student_view': # The block is acting as an XModule css_classes.append('xmodule_display') elif view == 'studio_view': # The block is acting as an XModuleDescriptor css_classes.append('xmodule_edit') css_classes.append('xmodule_' + class_name) data['type'] = block.js_module_name shim_xmodule_js(frag) if frag.js_init_fn: data['init'] = frag.js_init_fn data['runtime-class'] = runtime_class data['runtime-version'] = frag.js_init_version data['block-type'] = block.scope_ids.block_type data['usage-id'] = quote_slashes( unicode(block.scope_ids.usage_id).encode('utf-8')) template_context = { 'content': block.display_name if display_name_only else frag.content, 'classes': css_classes, 'display_name': block.display_name_with_default, 'data_attributes': ' '.join(u'data-{}="{}"'.format(key, value) for key, value in data.items()), } return wrap_fragment( frag, render_to_string('xblock_wrapper.html', template_context))
def get_url(self, dispatch): """Return item url with dispatch.""" return reverse( 'xblock_handler', args=(self.course.id, quote_slashes(self.item_url), 'xmodule_handler', dispatch) )
def test_escaped(self, test_string): self.assertNotIn('/', quote_slashes(test_string))
def test_inverse(self, test_string): self.assertEquals(test_string, unquote_slashes(quote_slashes(test_string)))
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