def handle_callback_and_get_module_info(self, mock_tracker, problem_display_name=None): """ Creates a fake module, invokes the callback and extracts the 'module' metadata 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.to_deprecated_string(), quote_slashes(descriptor.location.to_deprecated_string()), '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']
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 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', }) # assign correct identifier to each response. resp = self.client.post(modx_url, { get_input_id('{}_1').format(index): response for index, response in enumerate(responses, 2) }) return resp
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_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_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_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 get_handler_url(self, handler, xblock_name=None): """ Get url for the specified xblock handler """ if xblock_name is None: xblock_name = TestRecommender.XBLOCK_NAMES[0] return reverse('xblock_handler', kwargs={ 'course_id': self.course.id.to_deprecated_string(), 'usage_id': quote_slashes(self.course.id.make_usage_key('recommender', xblock_name).to_deprecated_string()), 'handler': handler, 'suffix': '' })
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 = 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.to_deprecated_string(), 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()), 'handler': 'xmodule_handler', 'suffix': 'problem_get', }) resp = self.client.post(modx_url, {}) return resp
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types): # 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": unicode(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), # 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': unicode(course_key)}), 'editor': email_editor, 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}), 'email_background_tasks_url': reverse('list_background_email_tasks', kwargs={'course_id': unicode(course_key)}), 'email_content_history_url': reverse('list_email_content', kwargs={'course_id': unicode(course_key)}), } return section_data
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id queries = GroupedQuery.objects.filter(course_id=course_key) query_options = tuple( (query.id, query.title or u'Query saved at ' + query.created.strftime("%m-%d-%y %H:%M")) for query in queries ) # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types): # 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": unicode(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), # 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'), 'keywords_supported': get_keywords_supported(), 'access': access, 'to_options': query_options, 'send_email': reverse('send_email', kwargs={'course_id': unicode(course_key)}), 'editor': email_editor, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': unicode(course_key)} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': unicode(course_key)} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': unicode(course_key)} ), } return section_data
def get_handler_url(self, handler, xblock_name=None): """ Get url for the specified xblock handler """ if xblock_name is None: xblock_name = TestRecommender.XBLOCK_NAMES[0] return reverse( "xblock_handler", kwargs={ "course_id": self.course.id.to_deprecated_string(), "usage_id": quote_slashes( self.course.id.make_usage_key("recommender", xblock_name).to_deprecated_string() ), "handler": handler, "suffix": "", }, )
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 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 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.to_deprecated_string(), 'usage_id': quote_slashes(problem_location.to_deprecated_string()), '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, 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 _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types): # 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": unicode(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), # 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() ) cohorts = [] if is_course_cohorted(course_key): cohorts = get_course_cohorts(course) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': unicode(course_key)}), 'editor': email_editor, 'cohorts': cohorts, 'default_cohort_name': DEFAULT_COHORT_NAME, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': unicode(course_key)} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': unicode(course_key)} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': unicode(course_key)} ), } 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. course_key = self.course.id return u'input_i4x-{0}-{1}-problem-{2}_{3}'.format( course_key.org.replace(u'.', u'_'), course_key.course.replace(u'.', u'_'), 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, self.course.id).to_deprecated_string()), 'handler': 'xmodule_handler', 'suffix': 'problem_check', }) # assign correct identifier to each response. resp = self.client.post( modx_url, { get_input_id(u'{}_1').format(index): response for index, response in enumerate(responses, 2) }) 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_handler_url(self, handler, xblock_name=None): """ Get url for the specified xblock handler """ if xblock_name is None: xblock_name = TestRecommender.XBLOCK_NAMES[0] return reverse('xblock_handler', kwargs={ 'course_id': self.course.id.to_deprecated_string(), 'usage_id': quote_slashes( self.course.id.make_usage_key( 'recommender', xblock_name).to_deprecated_string()), 'handler': handler, 'suffix': '' })
def test_wrap_xblock(self, course_id, data_usage_id): """ Verify that new content is added and the resources are the same. """ fragment = self.create_fragment(u"<h1>Test!</h1>") course = getattr(self, course_id) test_wrap_output = wrap_xblock( runtime_class='TestRuntime', block=course, view='baseview', frag=fragment, context=None, usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), request_token=uuid.uuid1().get_hex() ) self.assertIsInstance(test_wrap_output, Fragment) self.assertIn('xblock-baseview', test_wrap_output.content) self.assertIn('data-runtime-class="TestRuntime"', test_wrap_output.content) self.assertIn(data_usage_id, test_wrap_output.content) self.assertIn('<h1>Test!</h1>', test_wrap_output.content) self.assertEqual(test_wrap_output.resources[0].data, u'body {background-color:red;}') self.assertEqual(test_wrap_output.resources[1].data, 'alert("Hi!");')
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, "applicable_aside_types", null_applicable_aside_types): # 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": unicode(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), # 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": unicode(course_key)}), "editor": email_editor, "list_instructor_tasks_url": reverse("list_instructor_tasks", kwargs={"course_id": unicode(course_key)}), "email_background_tasks_url": reverse("list_background_email_tasks", kwargs={"course_id": unicode(course_key)}), "email_content_history_url": reverse("list_email_content", kwargs={"course_id": unicode(course_key)}), } return section_data
def test_missing_position_handler(self): """ Test that sending POST request without or invalid position argument don't raise server error """ self.client.login(username=self.mock_user.username, password="******") 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) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': ''}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': '-1'}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': "string"}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': u"Φυσικά"}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': None}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True})
def test_missing_position_handler(self): """ Test that sending POST request without or invalid position argument don't raise server error """ self.client.login(username=self.mock_user.username, password="******") 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) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': ''}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': '-1'}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': "string"}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': u"Φυσικά"}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': None}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True})
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.to_deprecated_string(), 'usage_id': quote_slashes( InstructorTaskModuleTestCase.problem_location( problem_url_name).to_deprecated_string()), 'handler': 'xmodule_handler', 'suffix': 'problem_get', }) resp = self.client.post(modx_url, {}) return resp
def test_escaped(self, test_string): self.assertNotIn('/', quote_slashes(test_string))
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, static_asset_path="", user_location=None, disable_staff_debug_info=False, course=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 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. Arguments: see arguments for get_module() request_token (str): A token unique to the request use by xblock initialization Returns: (LmsModuleSystem, KvsFieldData): (module system, student_data) bound to, primarily, the user and descriptor """ def make_xqueue_callback(dispatch="score_update"): """ Returns 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, } 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=user, descriptor=descriptor, student_data=student_data, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, course=course, ) def _fulfill_content_milestones(user, course_key, content_key): """ Internal helper to handle milestone fulfillments for the specified content module """ # Fulfillment Use Case: Entrance Exam # If this module is part of an entrance exam, we'll need to see if the student # has reached the point at which they can collect the associated milestone if milestones_helpers.is_entrance_exams_enabled(): course = modulestore().get_course(course_key) content = modulestore().get_item(content_key) entrance_exam_enabled = getattr(course, "entrance_exam_enabled", False) in_entrance_exam = getattr(content, "in_entrance_exam", False) if entrance_exam_enabled and in_entrance_exam: # We don't have access to the true request object in this context, but we can use a mock request = RequestFactory().request() request.user = user exam_pct = get_entrance_exam_score(request, course) if exam_pct >= course.entrance_exam_minimum_score_pct: exam_key = UsageKey.from_string(course.entrance_exam_id) relationship_types = milestones_helpers.get_milestone_relationship_types() content_milestones = milestones_helpers.get_course_content_milestones( course_key, exam_key, relationship=relationship_types["FULFILLS"] ) # Add each milestone to the user's set... user = {"id": request.user.id} for milestone in content_milestones: milestones_helpers.add_user_milestone(user, milestone) def handle_grade_event(block, event_type, event): # pylint: disable=unused-argument """ Manages the workflow for recording and updating of student module grade state """ user_id = user.id grade = event.get("value") max_grade = event.get("max_value") set_score(user_id, descriptor.location, grade, max_grade) # Bin score into range and increment stats score_bucket = get_score_bucket(grade, 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) # Cycle through the milestone fulfillment scenarios to see if any are now applicable # thanks to the updated grading information that was just submitted _fulfill_content_milestones(user, course_id, descriptor.location) # Send a signal out to any listeners who are waiting for score change # events. SCORE_CHANGED.send( sender=None, points_possible=event["max_value"], points_earned=event["value"], user_id=user_id, course_id=unicode(course_id), usage_id=unicode(descriptor.location), ) def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == "grade" and not is_masquerading_as_specific_student(user, course_id): handle_grade_event(block, event_type, event) else: aside_context = {} for aside in block.runtime.get_asides(block): if hasattr(aside, "get_event_context"): aside_event_info = aside.get_event_context(event_type, event) if aside_event_info is not None: aside_context[aside.scope_ids.block_type] = aside_event_info with tracker.get_tracker().context("asides", {"asides": aside_context}): 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, 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, 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, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, course=course, ) module.descriptor.bind_for_student( inner_system, real_user.id, [ partial(OverrideFieldData.wrap, real_user, course), partial(LmsFieldData, student_data=inner_student_data), ], ) module.descriptor.scope_ids = module.descriptor.scope_ids._replace(user_id=real_user.id) 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 = [] 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) # 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()), request_token=request_token, ) ) # 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 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 = bool(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 = bool(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 # 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) field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access user_is_staff = bool(has_access(user, u"staff", descriptor.location, course_id)) 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, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, 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={ "fs": FSService(), "field-data": field_data, "user": DjangoXBlockUserService(user, user_is_staff=user_is_staff), "reverification": ReverificationService(), "proctoring": ProctoringService(), "credit": CreditService(), "bookmarks": BookmarksService(user=user), }, get_user_role=lambda: get_user_role(user, course_id), descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access rebind_noauth_module_to_user=rebind_noauth_module_to_user, user_location=user_location, request_token=request_token, ) # 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) system.set(u"user_is_staff", user_is_staff) system.set(u"user_is_admin", bool(has_access(user, u"staff", "global"))) system.set(u"user_is_beta_tester", CourseBetaTesterRole(course_id).has_user(user)) system.set(u"days_early_for_beta", descriptor.days_early_for_beta) # 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, field_data
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, request_token, 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() request_token (str): A token unique to the request use by xblock initialization 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=user, descriptor=descriptor, field_data_cache=field_data_cache, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, ) def _fulfill_content_milestones(user, course_key, content_key): """ Internal helper to handle milestone fulfillments for the specified content module """ # Fulfillment Use Case: Entrance Exam # If this module is part of an entrance exam, we'll need to see if the student # has reached the point at which they can collect the associated milestone if settings.FEATURES.get('ENTRANCE_EXAMS', False): course = modulestore().get_course(course_key) content = modulestore().get_item(content_key) entrance_exam_enabled = getattr(course, 'entrance_exam_enabled', False) in_entrance_exam = getattr(content, 'in_entrance_exam', False) if entrance_exam_enabled and in_entrance_exam: # We don't have access to the true request object in this context, but we can use a mock request = RequestFactory().request() request.user = user exam_pct = get_entrance_exam_score(request, course) if exam_pct >= course.entrance_exam_minimum_score_pct: exam_key = UsageKey.from_string(course.entrance_exam_id) relationship_types = milestones_helpers.get_milestone_relationship_types() content_milestones = milestones_helpers.get_course_content_milestones( course_key, exam_key, relationship=relationship_types['FULFILLS'] ) # Add each milestone to the user's set... user = {'id': request.user.id} for milestone in content_milestones: milestones_helpers.add_user_milestone(user, milestone) def handle_grade_event(block, event_type, event): # pylint: disable=unused-argument """ Manages the workflow for recording and updating of student module grade state """ 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) # Cycle through the milestone fulfillment scenarios to see if any are now applicable # thanks to the updated grading information that was just submitted _fulfill_content_milestones( user, course_id, descriptor.location, ) 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, asides=XBlockAsidesConfig.possible_asides(), ) (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 descriptor=module.descriptor, course_id=course_id, track_function=track_function, xqueue_callback_url_prefix=xqueue_callback_url_prefix, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, request_token=request_token ) # 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 real_user.id, ) 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()), request_token=request_token, )) # 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) field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access user_is_staff = has_access(user, u'staff', descriptor.location, course_id) 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)), get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, 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(), 'fs': xblock.reference.plugins.FSService(), 'field-data': field_data, 'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff), }, get_user_role=lambda: get_user_role(user, course_id), descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access rebind_noauth_module_to_user=rebind_noauth_module_to_user, user_location=user_location, request_token=request_token, ) # 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') and user.is_authenticated(): 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', user_is_staff) system.set(u'user_is_admin', has_access(user, u'staff', 'global')) system.set(u'user_is_beta_tester', CourseBetaTesterRole(course_id).has_user(user)) system.set(u'days_early_for_beta', getattr(descriptor, 'days_early_for_beta')) # 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, field_data
def get_url(self, dispatch): """Return item url with dispatch.""" return reverse( 'xblock_handler', args=(unicode(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch) )
def test_inverse(self, test_string): self.assertEquals(test_string, unquote_slashes(quote_slashes(test_string)))
def get_url(self, dispatch): """Return item url with dispatch.""" return reverse('xblock_handler', args=(self.course.id.to_deprecated_string(), quote_slashes(self.item_url), 'xmodule_handler', dispatch))
def get_url(self, dispatch): """Return item url with dispatch.""" return reverse('xblock_handler', args=(unicode(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch))
def get_url(self, dispatch): """Return item url with dispatch.""" return reverse( 'xblock_handler', args=(self.course.id.to_deprecated_string(), quote_slashes(self.item_url), 'xmodule_handler', dispatch) )