def id_to_location(course_id): '''Convert the given course_id (org/course/name) to a location object. Throws ValueError if course_id is of the wrong format. ''' org, course, name = course_id.split('/') return Location('i4x', org, course, 'course', name)
def test_jumpto_invalid_location(self): location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None) jumpto_url = '%s/%s/jump_to/%s' % ( '/courses', self.course_name, location) response = self.client.get(jumpto_url) self.assertEqual(response.status_code, 404)
def test_equality(): assert_equals(Location('tag', 'org', 'course', 'category', 'name'), Location('tag', 'org', 'course', 'category', 'name')) assert_not_equals(Location('tag', 'org', 'course', 'category', 'name1'), Location('tag', 'org', 'course', 'category', 'name'))
class CombinedOpenEndedModuleTest(unittest.TestCase): """ Unit tests for the combined open ended xmodule """ location = Location( ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) definition_template = """ <combinedopenended attempts="10000"> {rubric} {prompt} <task> {task1} </task> <task> {task2} </task> </combinedopenended> """ prompt = "<prompt>This is a question prompt</prompt>" rubric = '''<rubric><rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>Second option</option> </category> </rubric></rubric>''' max_score = 1 metadata = {'attempts': '10', 'max_score': max_score} static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': "", 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, 'graded': True, } oeparam = etree.XML(''' <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> ''') task_xml1 = ''' <selfassessment> <hintprompt> What hint about this problem would you give to someone? </hintprompt> <submitmessage> Save Succcesful. Thanks for participating! </submitmessage> </selfassessment> ''' task_xml2 = ''' <openended min_score_to_attempt="1" max_score_to_attempt="1"> <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> </openended>''' definition = { 'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2] } full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) descriptor = Mock(data=full_definition) test_system = get_test_system() test_system.open_ended_grading_interface = None combinedoe_container = CombinedOpenEndedModule( descriptor=descriptor, runtime=test_system, field_data=DictFieldData({ 'data': full_definition, 'weight': '1', }), scope_ids=ScopeIds(None, None, None, None), ) def setUp(self): self.combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, self.definition, self.descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=self.static_data) def test_get_tag_name(self): """ Test to see if the xml tag name is correct """ name = self.combinedoe.get_tag_name("<t>Tag</t>") self.assertEqual(name, "t") def test_get_last_response(self): """ See if we can parse the last response """ response_dict = self.combinedoe.get_last_response(0) self.assertEqual(response_dict['type'], "selfassessment") self.assertEqual(response_dict['max_score'], self.max_score) self.assertEqual(response_dict['state'], CombinedOpenEndedV1Module.INITIAL) def test_update_task_states(self): """ See if we can update the task states properly """ changed = self.combinedoe.update_task_states() self.assertFalse(changed) current_task = self.combinedoe.current_task current_task.change_state(CombinedOpenEndedV1Module.DONE) changed = self.combinedoe.update_task_states() self.assertTrue(changed) def test_get_max_score(self): """ Try to get the max score of the problem """ self.combinedoe.update_task_states() self.combinedoe.state = "done" self.combinedoe.is_scored = True max_score = self.combinedoe.max_score() self.assertEqual(max_score, 1) def test_container_get_max_score(self): """ See if we can get the max score from the actual xmodule """ #The progress view requires that this function be exposed max_score = self.combinedoe_container.max_score() self.assertEqual(max_score, None) def test_container_get_progress(self): """ See if we can get the progress from the actual xmodule """ progress = self.combinedoe_container.max_score() self.assertEqual(progress, None) def test_get_progress(self): """ Test if we can get the correct progress from the combined open ended class """ self.combinedoe.update_task_states() self.combinedoe.state = "done" self.combinedoe.is_scored = True progress = self.combinedoe.get_progress() self.assertIsInstance(progress, Progress) # progress._a is the score of the xmodule, which is 0 right now. self.assertEqual(progress._a, 0) # progress._b is the max_score (which is 1), divided by the weight (which is 1). self.assertEqual(progress._b, 1) def test_container_weight(self): """ Check the problem weight in the container """ weight = self.combinedoe_container.weight self.assertEqual(weight, 1) def test_container_child_weight(self): """ Test the class to see if it picks up the right weight """ weight = self.combinedoe_container.child_module.weight self.assertEqual(weight, 1) def test_get_score(self): """ See if scoring works """ score_dict = self.combinedoe.get_score() self.assertEqual(score_dict['score'], 0) self.assertEqual(score_dict['total'], 1) def test_alternate_orderings(self): """ Try multiple ordering of definitions to see if the problem renders different steps correctly. """ t1 = self.task_xml1 t2 = self.task_xml2 xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1], [t1, t2, t1]] for xml in xml_to_test: definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml } descriptor = Mock(data=definition) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=self.static_data) changed = combinedoe.update_task_states() self.assertFalse(changed) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state={'task_states': TEST_STATE_SA}) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state={'task_states': TEST_STATE_SA_IN}) def test_get_score_realistic(self): """ Try to parse the correct score from a json instance state """ instance_state = json.loads(MOCK_INSTANCE_STATE) rubric = """ <rubric> <rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option> <option>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option> <option>The response is correct, complete, and contains evidence of higher-order thinking.</option> </category> </rubric> </rubric> """ definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(rubric), 'task_xml': [self.task_xml1, self.task_xml2] } descriptor = Mock(data=definition) combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=instance_state) score_dict = combinedoe.get_score() self.assertEqual(score_dict['score'], 15.0) self.assertEqual(score_dict['total'], 15.0) def generate_oe_module(self, task_state, task_number, task_xml): """ Return a combined open ended module with the specified parameters """ definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': task_xml } descriptor = Mock(data=definition) instance_state = {'task_states': task_state, 'graded': True} if task_number is not None: instance_state.update({'current_task_number': task_number}) combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=instance_state) return combinedoe def ai_state_reset(self, task_state, task_number=None): """ See if state is properly reset """ combinedoe = self.generate_oe_module(task_state, task_number, [self.task_xml2]) html = combinedoe.get_html() self.assertIsInstance(html, basestring) score = combinedoe.get_score() if combinedoe.is_scored: self.assertEqual(score['score'], 0) else: self.assertEqual(score['score'], None) def ai_state_success(self, task_state, task_number=None, iscore=2, tasks=None): """ See if state stays the same """ if tasks is None: tasks = [self.task_xml1, self.task_xml2] combinedoe = self.generate_oe_module(task_state, task_number, tasks) html = combinedoe.get_html() self.assertIsInstance(html, basestring) score = combinedoe.get_score() self.assertEqual(int(score['score']), iscore) def test_ai_state_reset(self): self.ai_state_reset(TEST_STATE_AI) def test_ai_state2_reset(self): self.ai_state_reset(TEST_STATE_AI2) def test_ai_invalid_state(self): self.ai_state_reset(TEST_STATE_AI2_INVALID) def test_ai_state_rest_task_number(self): self.ai_state_reset(TEST_STATE_AI, task_number=2) self.ai_state_reset(TEST_STATE_AI, task_number=5) self.ai_state_reset(TEST_STATE_AI, task_number=1) self.ai_state_reset(TEST_STATE_AI, task_number=0) def test_ai_state_success(self): self.ai_state_success(TEST_STATE_AI) def test_state_single(self): self.ai_state_success(TEST_STATE_SINGLE, iscore=12) def test_state_pe_single(self): self.ai_state_success(TEST_STATE_PE_SINGLE, iscore=0, tasks=[self.task_xml2])
class OpenEndedModuleXmlImageUploadTest(unittest.TestCase, DummyModulestore): """ Test if student is able to upload images properly. """ problem_location = Location([ "i4x", "edX", "open_ended", "combinedopenended", "SampleQuestionImageUpload" ]) answer_text = "Hello, this is my amazing answer." file_text = "Hello, this is my amazing file." file_name = "Student file 1" answer_link = "http://www.edx.org" autolink_tag = "<a href=" def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.test_system.s3_interface = test_util_open_ended.S3_INTERFACE self.test_system.xqueue['interface'] = Mock(send_to_queue=Mock( side_effect=[1, "queued"])) self.setup_modulestore(COURSE) def test_file_upload_fail(self): """ Test to see if a student submission without a file attached fails. """ module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer response = module.handle_ajax("save_answer", {"student_answer": self.answer_text}) response = json.loads(response) self.assertFalse(response['success']) self.assertIn('error', response) @patch('xmodule.open_ended_grading_classes.openendedchild.S3Connection', test_util_open_ended.MockS3Connection) @patch('xmodule.open_ended_grading_classes.openendedchild.Key', test_util_open_ended.MockS3Key) def test_file_upload_success(self): """ Test to see if a student submission with a file is handled properly. """ module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer with a file response = module.handle_ajax( "save_answer", { "student_answer": self.answer_text, "valid_files_attached": True, "student_file": [MockUploadedFile(self.file_name, self.file_text)], }) response = json.loads(response) self.assertTrue(response['success']) self.assertIn(self.file_name, response['student_response']) self.assertIn(self.autolink_tag, response['student_response']) def test_link_submission_success(self): """ Students can submit links instead of files. Check that the link is properly handled. """ module = self.get_module_from_location(self.problem_location, COURSE) # Simulate a student saving an answer with a link. response = module.handle_ajax("save_answer", { "student_answer": "{0} {1}".format(self.answer_text, self.answer_link) }) response = json.loads(response) self.assertTrue(response['success']) self.assertIn(self.answer_link, response['student_response']) self.assertIn(self.autolink_tag, response['student_response'])
def test_immutable(self, attr): loc = Location('t://o/c/c/n@r') with self.assertRaises(AttributeError): setattr(loc, attr, attr)
class OpenEndedModuleTest(unittest.TestCase): """ Test the open ended module class """ location = Location( ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) metadata = json.dumps({'attempts': '10'}) prompt = etree.XML("<prompt>This is a question prompt</prompt>") rubric = etree.XML('''<rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> </category> </rubric>''') max_score = 4 static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': None, 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, 'control': { 'required_peer_grading': 1, 'peer_grader_count': 1, 'min_to_calibrate': 3, 'max_to_calibrate': 6, 'peer_grade_finished_submissions_when_none_pending': False, } } oeparam = etree.XML(''' <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> ''') definition = {'oeparam': oeparam} descriptor = Mock() def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") def constructed_callback(dispatch="score_update"): return dispatch self.test_system.xqueue = { 'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', 'waittime': 1 } self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_message_post(self): get = { 'feedback': 'feedback text', 'submission_id': '1', 'grader_id': '1', 'score': 3 } qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) student_info = { 'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime } contents = { 'feedback': get['feedback'], 'submission_id': int(get['submission_id']), 'grader_id': int(get['grader_id']), 'score': get['score'], 'student_info': json.dumps(student_info) } result = self.openendedmodule.message_post(get, self.test_system) self.assertTrue(result['success']) # make sure it's actually sending something we want to the queue self.mock_xqueue.send_to_queue.assert_called_with( body=json.dumps(contents), header=ANY) state = json.loads(self.openendedmodule.get_instance_state()) self.assertIsNotNone(state['child_state'], OpenEndedModule.DONE) def test_send_to_grader(self): submission = "This is a student submission" qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) student_info = { 'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime } contents = self.openendedmodule.payload.copy() contents.update({ 'student_info': json.dumps(student_info), 'student_response': submission, 'max_score': self.max_score }) result = self.openendedmodule.send_to_grader(submission, self.test_system) self.assertTrue(result) self.mock_xqueue.send_to_queue.assert_called_with( body=json.dumps(contents), header=ANY) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") score_msg = { 'correct': True, 'score': 4, 'msg': 'Grader Message', 'feedback': "Grader Feedback" } get = {'queuekey': "abcd", 'xqueue_body': score_msg} self.openendedmodule.update_score(get, self.test_system) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") feedback = {"success": True, "feedback": "Grader Feedback"} score_msg = { 'correct': True, 'score': 4, 'msg': 'Grader Message', 'feedback': json.dumps(feedback), 'grader_type': 'IN', 'grader_id': '1', 'submission_id': '1', 'success': True, 'rubric_scores': [0], 'rubric_scores_complete': True, 'rubric_xml': etree.tostring(self.rubric) } get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)} self.openendedmodule.update_score(get, self.test_system) def update_score_multiple(self): self.openendedmodule.new_history_entry("New Entry") feedback = {"success": True, "feedback": "Grader Feedback"} score_msg = { 'correct': True, 'score': [0, 1], 'msg': 'Grader Message', 'feedback': [json.dumps(feedback), json.dumps(feedback)], 'grader_type': 'PE', 'grader_id': ['1', '2'], 'submission_id': '1', 'success': True, 'rubric_scores': [[0], [0]], 'rubric_scores_complete': [True, True], 'rubric_xml': [etree.tostring(self.rubric), etree.tostring(self.rubric)] } get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)} self.openendedmodule.update_score(get, self.test_system) def test_latest_post_assessment(self): self.update_score_single() assessment = self.openendedmodule.latest_post_assessment( self.test_system) self.assertFalse(assessment == '') # check for errors self.assertFalse('errors' in assessment) def test_update_score_single(self): self.update_score_single() score = self.openendedmodule.latest_score() self.assertEqual(score, 4) def test_update_score_multiple(self): """ Tests that a score of [0, 1] gets aggregated to 1. A change in behavior added by @jbau """ self.update_score_multiple() score = self.openendedmodule.latest_score() self.assertEquals(score, 1) def test_open_ended_display(self): """ Test storing answer with the open ended module. """ # Create a module with no state yet. Important that this start off as a blank slate. test_module = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) saved_response = "Saved response." submitted_response = "Submitted response." # Initially, there will be no stored answer. self.assertEqual(test_module.stored_answer, None) # And the initial answer to display will be an empty string. self.assertEqual(test_module.get_display_answer(), "") # Now, store an answer in the module. test_module.handle_ajax("store_answer", {'student_answer': saved_response}, get_test_system()) # The stored answer should now equal our response. self.assertEqual(test_module.stored_answer, saved_response) self.assertEqual(test_module.get_display_answer(), saved_response) # Mock out the send_to_grader function so it doesn't try to connect to the xqueue. test_module.send_to_grader = Mock(return_value=True) # Submit a student response to the question. test_module.handle_ajax("save_answer", {"student_answer": submitted_response}, get_test_system()) # Submitting an answer should clear the stored answer. self.assertEqual(test_module.stored_answer, None) # Confirm that the answer is stored properly. self.assertEqual(test_module.latest_answer(), submitted_response)
def add_repo(repo, rdir_in): """This will add a git repo into the mongo modulestore""" # pylint: disable=R0915 # Set defaults even if it isn't defined in settings mongo_db = { 'host': 'localhost', 'user': '', 'password': '', 'db': 'xlog', } # Allow overrides if hasattr(settings, 'MONGODB_LOG'): for config_item in [ 'host', 'user', 'password', 'db', ]: mongo_db[config_item] = settings.MONGODB_LOG.get( config_item, mongo_db[config_item]) if not os.path.isdir(GIT_REPO_DIR): raise GitImportError(GitImportError.NO_DIR) # pull from git if not (repo.endswith('.git') or repo.startswith( ('http:', 'https:', 'git:', 'file:'))): raise GitImportError(GitImportError.URL_BAD) if rdir_in: rdir = os.path.basename(rdir_in) else: rdir = repo.rsplit('/', 1)[-1].rsplit('.git', 1)[0] log.debug('rdir = {0}'.format(rdir)) rdirp = '{0}/{1}'.format(GIT_REPO_DIR, rdir) if os.path.exists(rdirp): log.info('directory already exists, doing a git pull instead ' 'of git clone') cmd = [ 'git', 'pull', ] cwd = rdirp else: cmd = [ 'git', 'clone', repo, ] cwd = GIT_REPO_DIR cwd = os.path.abspath(cwd) try: ret_git = cmd_log(cmd, cwd=cwd) except subprocess.CalledProcessError as ex: log.exception('Error running git pull: %r', ex.output) raise GitImportError(GitImportError.CANNOT_PULL) # get commit id cmd = [ 'git', 'log', '-1', '--format=%H', ] try: commit_id = cmd_log(cmd, cwd=rdirp) except subprocess.CalledProcessError as ex: log.exception('Unable to get git log: %r', ex.output) raise GitImportError(GitImportError.BAD_REPO) ret_git += '\nCommit ID: {0}'.format(commit_id) # get branch cmd = [ 'git', 'symbolic-ref', '--short', 'HEAD', ] try: branch = cmd_log(cmd, cwd=rdirp) except subprocess.CalledProcessError as ex: # I can't discover a way to excercise this, but git is complex # so still logging and raising here in case. log.exception('Unable to determine branch: %r', ex.output) raise GitImportError(GitImportError.BAD_REPO) ret_git += '{0}Branch: {1}'.format(' \n', branch) # Get XML logging logger and capture debug to parse results output = StringIO.StringIO() import_log_handler = logging.StreamHandler(output) import_log_handler.setLevel(logging.DEBUG) logger_names = [ 'xmodule.modulestore.xml_importer', 'git_add_course', 'xmodule.modulestore.xml', 'xmodule.seq_module', ] loggers = [] for logger_name in logger_names: logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) logger.addHandler(import_log_handler) loggers.append(logger) try: management.call_command('import', GIT_REPO_DIR, rdir, nostatic=not GIT_IMPORT_STATIC) except CommandError: raise GitImportError(GitImportError.XML_IMPORT_FAILED) except NotImplementedError: raise GitImportError(GitImportError.UNSUPPORTED_STORE) ret_import = output.getvalue() # Remove handler hijacks for logger in loggers: logger.setLevel(logging.NOTSET) logger.removeHandler(import_log_handler) course_id = 'unknown' location = 'unknown' # extract course ID from output of import-command-run and make symlink # this is needed in order for custom course scripts to work match = re.search('(?ms)===> IMPORTING course to location (\S+)', ret_import) if match: location = Location(match.group(1)) log.debug('location = {0}'.format(location)) course_id = location.course_id cdir = '{0}/{1}'.format(GIT_REPO_DIR, location.course) log.debug('Studio course dir = {0}'.format(cdir)) if os.path.exists(cdir) and not os.path.islink(cdir): log.debug(' -> exists, but is not symlink') log.debug( subprocess.check_output([ 'ls', '-l', ], cwd=os.path.abspath(cdir))) try: os.rmdir(os.path.abspath(cdir)) except OSError: log.exception('Failed to remove course directory') if not os.path.exists(cdir): log.debug(' -> creating symlink between {0} and {1}'.format( rdirp, cdir)) try: os.symlink(os.path.abspath(rdirp), os.path.abspath(cdir)) except OSError: log.exception('Unable to create course symlink') log.debug( subprocess.check_output([ 'ls', '-l', ], cwd=os.path.abspath(cdir))) # store import-command-run output in mongo mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db) try: if mongo_db['user'] and mongo_db['password']: mdb = mongoengine.connect(mongo_db['db'], host=mongouri) else: mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host']) except mongoengine.connection.ConnectionError: log.exception('Unable to connect to mongodb to save log, please ' 'check MONGODB_LOG settings') cil = CourseImportLog( course_id=course_id, location=unicode(location), repo_dir=rdir, created=timezone.now(), import_log=ret_import, git_log=ret_git, ) cil.save() log.debug('saved CourseImportLog for {0}'.format(cil.course_id)) mdb.disconnect()
def test_static_url_generation(self): location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg']) path = StaticContent.get_static_path_from_location(location) self.assertEquals(path, '/static/my_file_name.jpg')
def compute_metadata_inheritance_tree(self, location): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here query = { '_id.org': location.org, '_id.course': location.course, '_id.category': { '$in': [ 'course', 'chapter', 'sequential', 'vertical', 'videosequence', 'wrapper', 'problemset', 'conditional', 'randomize' ] } } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} # just get the inheritable metadata since that is all we need for the computation # this minimizes both data pushed over the wire for field_name in InheritanceMixin.fields: record_filter['metadata.{0}'.format(field_name)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) results_by_url = {} root = None # now go through the results and order them by the location url for result in resultset: location = Location(result['_id']) # We need to collate between draft and non-draft # i.e. draft verticals can have children which are not in non-draft versions location = location.replace(revision=None) location_url = location.url() if location_url in results_by_url: existing_children = results_by_url[location_url].get( 'definition', {}).get('children', []) additional_children = result.get('definition', {}).get('children', []) total_children = existing_children + additional_children if 'definition' not in results_by_url[location_url]: results_by_url[location_url]['definition'] = {} results_by_url[location_url]['definition'][ 'children'] = total_children results_by_url[location.url()] = result if location.category == 'course': root = location.url() # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} def _compute_inherited_metadata(url): """ Helper method for computing inherited metadata for a specific location url """ # check for presence of metadata key. Note that a given module may not yet be fully formed. # example: update_item -> update_children -> update_metadata sequence on new item create # if we get called here without update_metadata called first then 'metadata' hasn't been set # as we're not fully transactional at the DB layer. Same comment applies to below key name # check my_metadata = results_by_url[url].get('metadata', {}) # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes for child in results_by_url[url].get('definition', {}).get('children', []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get( 'metadata', {})) results_by_url[child]['metadata'] = new_child_metadata metadata_to_inherit[child] = new_child_metadata _compute_inherited_metadata(child) else: # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata if root is not None: _compute_inherited_metadata(root) return metadata_to_inherit
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False): # check to see if the dest_location exists as an empty course # we need an empty course because the app layers manage the permissions and users if not modulestore.has_item(dest_location.course_id, dest_location): raise Exception( "An empty course at {0} must have already been created. Aborting..." .format(dest_location)) # verify that the dest_location really is an empty course, which means only one with an optional 'overview' dest_modules = modulestore.get_items([ dest_location.tag, dest_location.org, dest_location.course, None, None, None ]) basically_empty = True for module in dest_modules: if module.location.category == 'course' or ( module.location.category == 'about' and module.location.name == 'overview'): continue basically_empty = False break if not basically_empty: raise Exception( "Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting..." .format(dest_location)) # check to see if the source course is actually there if not modulestore.has_item(source_location.course_id, source_location): raise Exception( "Cannot find a course at {0}. Aborting".format(source_location)) # Get all modules under this namespace which is (tag, org, course) tuple modules = modulestore.get_items([ source_location.tag, source_location.org, source_location.course, None, None, None ]) _clone_modules(modulestore, modules, source_location, dest_location) modules = modulestore.get_items([ source_location.tag, source_location.org, source_location.course, None, None, 'draft' ]) _clone_modules(modulestore, modules, source_location, dest_location) # now iterate through all of the assets and clone them # first the thumbnails thumbs = contentstore.get_all_content_thumbnails_for_course( source_location) for thumb in thumbs: thumb_loc = Location(thumb["_id"]) content = contentstore.find(thumb_loc) content.location = content.location._replace( org=dest_location.org, course=dest_location.course) print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location) contentstore.save(content) # now iterate through all of the assets, also updating the thumbnail pointer assets, __ = contentstore.get_all_content_for_course(source_location) for asset in assets: asset_loc = Location(asset["_id"]) content = contentstore.find(asset_loc) content.location = content.location._replace( org=dest_location.org, course=dest_location.course) # be sure to update the pointer to the thumbnail if content.thumbnail_location is not None: content.thumbnail_location = content.thumbnail_location._replace( org=dest_location.org, course=dest_location.course) print "Cloning asset {0} to {1}".format(asset_loc, content.location) contentstore.save(content) return True
def create(graceperiod=None, due=None, max_attempts=None, showanswer=None, rerandomize=None, force_save_button=None, attempts=None, problem_state=None, correct=False, done=None): """ All parameters are optional, and are added to the created problem if specified. Arguments: graceperiod: due: max_attempts: showanswer: force_save_button: rerandomize: all strings, as specified in the policy for the problem problem_state: a dict to to be serialized into the instance_state of the module. attempts: also added to instance state. Will be converted to an int. """ location = Location([ "i4x", "edX", "capa_test", "problem", "SampleProblem{0}".format(CapaFactory.next_num()) ]) model_data = {'data': CapaFactory.sample_problem_xml} if graceperiod is not None: model_data['graceperiod'] = graceperiod if due is not None: model_data['due'] = due if max_attempts is not None: model_data['max_attempts'] = max_attempts if showanswer is not None: model_data['showanswer'] = showanswer if force_save_button is not None: model_data['force_save_button'] = force_save_button if rerandomize is not None: model_data['rerandomize'] = rerandomize if done is not None: model_data['done'] = done descriptor = Mock(weight="1") if problem_state is not None: model_data.update(problem_state) if attempts is not None: # converting to int here because I keep putting "0" and "1" in the tests # since everything else is a string. model_data['attempts'] = int(attempts) system = test_system() system.render_template = Mock( return_value="<div>Test Template HTML</div>") module = CapaModule(system, location, descriptor, model_data) if correct: # TODO: probably better to actually set the internal state properly, but... module.get_score = lambda: {'score': 1, 'total': 1} else: module.get_score = lambda: {'score': 0, 'total': 1} return module
def test_encode_location(self): loc = Location('i4x', 'org', 'course', 'category', 'name') self.assertEqual(loc.url(), self.encoder.default(loc)) loc = Location('i4x', 'org', 'course', 'category', 'name', 'version') self.assertEqual(loc.url(), self.encoder.default(loc))
def test_center_login(request): ''' Log in students taking exams via Pearson Takes a POST request that contains the following keys: - code - a security code provided by Pearson - clientCandidateID - registrationID - exitURL - the url that we redirect to once we're done - vueExamSeriesCode - a code that indicates the exam that we're using ''' # errors are returned by navigating to the error_url, adding a query parameter named "code" # which contains the error code describing the exceptional condition. def makeErrorURL(error_url, error_code): log.error("generating error URL with error code {}".format(error_code)) return "{}?code={}".format(error_url, error_code) # get provided error URL, which will be used as a known prefix for returning error messages to the # Pearson shell. error_url = request.POST.get("errorURL") # TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson # with the code we calculate for the same parameters. if 'code' not in request.POST: return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode")) code = request.POST.get("code") # calculate SHA for query string # TODO: figure out how to get the original query string, so we can hash it # and compare. if 'clientCandidateID' not in request.POST: return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID")) client_candidate_id = request.POST.get("clientCandidateID") # TODO: check remaining parameters, and maybe at least log if they're not matching # expected values.... # registration_id = request.POST.get("registrationID") # exit_url = request.POST.get("exitURL") # find testcenter_user that matches the provided ID: try: testcenteruser = TestCenterUser.objects.get( client_candidate_id=client_candidate_id) except TestCenterUser.DoesNotExist: log.error("not able to find demographics for cand ID {}".format( client_candidate_id)) return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")) # find testcenter_registration that matches the provided exam code: # Note that we could rely in future on either the registrationId or the exam code, # or possibly both. But for now we know what to do with an ExamSeriesCode, # while we currently have no record of RegistrationID values at all. if 'vueExamSeriesCode' not in request.POST: # we are not allowed to make up a new error code, according to Pearson, # so instead of "missingExamSeriesCode", we use a valid one that is # inaccurate but at least distinct. (Sigh.) log.error("missing exam series code for cand ID {}".format( client_candidate_id)) return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID")) exam_series_code = request.POST.get('vueExamSeriesCode') registrations = TestCenterRegistration.objects.filter( testcenter_user=testcenteruser, exam_series_code=exam_series_code) if not registrations: log.error("not able to find exam registration for exam {} and cand ID {}".format( exam_series_code, client_candidate_id)) return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned")) # TODO: figure out what to do if there are more than one registrations.... # for now, just take the first... registration = registrations[0] course_id = registration.course_id course = course_from_id(course_id) # assume it will be found.... if not course: log.error("not able to find course from ID {} for cand ID {}".format( course_id, client_candidate_id)) return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")) exam = course.get_test_center_exam(exam_series_code) if not exam: log.error("not able to find exam {} for course ID {} and cand ID {}".format( exam_series_code, course_id, client_candidate_id)) return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")) location = exam.exam_url log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format( client_candidate_id, exam_series_code, course_id, location)) # check if the test has already been taken timelimit_descriptor = modulestore().get_instance( course_id, Location(location)) if not timelimit_descriptor: log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format( client_candidate_id, exam_series_code, course_id, location)) return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents( course_id, testcenteruser.user, timelimit_descriptor, depth=None) timelimit_module = get_module_for_descriptor( request.user, request, timelimit_descriptor, timelimit_module_cache, course_id, position=None) if not timelimit_module.category == 'timelimit': log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format( client_candidate_id, exam_series_code, course_id, location)) return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) if timelimit_module and timelimit_module.has_ended: log.warning("cand {} on exam {} for course {}: test already over at {}".format( client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at)) return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken")) # check if we need to provide an accommodation: time_accommodation_mapping = {'ET12ET': 'ADDHALFTIME', 'ET30MN': 'ADD30MIN', 'ETDBTM': 'ADDDOUBLE', } time_accommodation_code = None for code in registration.get_accommodation_codes(): if code in time_accommodation_mapping: time_accommodation_code = time_accommodation_mapping[code] if time_accommodation_code: timelimit_module.accommodation_code = time_accommodation_code log.info("cand {} on exam {} for course {}: receiving accommodation {}".format( client_candidate_id, exam_series_code, course_id, time_accommodation_code)) # UGLY HACK!!! # Login assumes that authentication has occurred, and that there is a # backend annotation on the user object, indicating which backend # against which the user was authenticated. We're authenticating here # against the registration entry, and assuming that the request given # this information is correct, we allow the user to be logged in # without a password. This could all be formalized in a backend object # that does the above checking. # TODO: (brian) create a backend class to do this. # testcenteruser.user.backend = "%s.%s" % (backend.__module__, # backend.__class__.__name__) testcenteruser.user.backend = "%s.%s" % ( "TestcenterAuthenticationModule", "TestcenterAuthenticationClass") login(request, testcenteruser.user) # And start the test: return jump_to(request, course_id, location)
def test_none(self): self.assertEquals([None] * 6, Location(None).list())
def get_hints(request, course_id, field): """ Load all of the hints submitted to the course. Args: `request` -- Django request object. `course_id` -- The course id, like 'Me/19.002/test_course' `field` -- Either 'hints' or 'mod_queue'; specifies which set of hints to load. Keys in returned dict: - 'field': Same as input - 'other_field': 'mod_queue' if `field` == 'hints'; and vice-versa. - 'field_label', 'other_field_label': English name for the above. - 'all_hints': A list of [answer, pk dict] pairs, representing all hints. Sorted by answer. - 'id_to_name': A dictionary mapping problem id to problem name. """ if field == 'mod_queue': other_field = 'hints' field_label = 'Hints Awaiting Moderation' other_field_label = 'Approved Hints' elif field == 'hints': other_field = 'mod_queue' field_label = 'Approved Hints' other_field_label = 'Hints Awaiting Moderation' # The course_id is of the form school/number/classname. # We want to use the course_id to find all matching definition_id's. # To do this, just take the school/number part - leave off the classname. chopped_id = '/'.join(course_id.split('/')[:-1]) chopped_id = re.escape(chopped_id) all_hints = XModuleContentField.objects.filter( field_name=field, definition_id__regex=chopped_id) # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer] # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer. big_out_dict = {} # id_to name maps a problem id to the name of the problem. # id_to_name[problem id] = Display name of problem id_to_name = {} for hints_by_problem in all_hints: loc = Location(hints_by_problem.definition_id) name = location_to_problem_name(course_id, loc) if name is None: continue id_to_name[hints_by_problem.definition_id] = name def answer_sorter(thing): """ `thing` is a tuple, where `thing[0]` contains an answer, and `thing[1]` contains a dict of hints. This function returns an index based on `thing[0]`, which is used as a key to sort the list of things. """ try: return float(thing[0]) except ValueError: # Put all non-numerical answers first. return float('-inf') # Answer list contains [answer, dict_of_hints] pairs. answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter) big_out_dict[hints_by_problem.definition_id] = answer_list render_dict = { 'field': field, 'other_field': other_field, 'field_label': field_label, 'other_field_label': other_field_label, 'all_hints': big_out_dict, 'id_to_name': id_to_name } return render_dict
def test_invalid_locations(self, loc): with self.assertRaises(InvalidLocationError): Location(loc)
def location(self): return Location('i4x://org/course/category/{}'.format(self.url_name))
def test_string_roundtrip(self, url): self.assertEquals(url, Location(url).url()) self.assertEquals(url, str(Location(url)))
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False): # first check to see if the modulestore is Mongo backed if not isinstance(modulestore, MongoModuleStore): raise Exception( "Expected a MongoModuleStore in the runtime. Aborting....") # check to see if the dest_location exists as an empty course # we need an empty course because the app layers manage the permissions and users if not modulestore.has_item(dest_location): raise Exception( "An empty course at {0} must have already been created. Aborting..." .format(dest_location)) # verify that the dest_location really is an empty course, which means only one with an optional 'overview' dest_modules = modulestore.get_items([ dest_location.tag, dest_location.org, dest_location.course, None, None, None ]) basically_empty = True for module in dest_modules: if module.location.category == 'course' or ( module.location.category == 'about' and module.location.name == 'overview'): continue basically_empty = False break if not basically_empty: raise Exception( "Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting..." .format(dest_location)) # check to see if the source course is actually there if not modulestore.has_item(source_location): raise Exception( "Cannot find a course at {0}. Aborting".format(source_location)) # Get all modules under this namespace which is (tag, org, course) tuple modules = modulestore.get_items([ source_location.tag, source_location.org, source_location.course, None, None, None ]) for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course) else: # on the course module we also have to update the module name module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name) print "Cloning module {0} to {1}....".format(original_loc, module.location) modulestore.update_item(module.location, module._model_data._kvs._data) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org, course=dest_location.course) new_children.append(child_loc.url()) modulestore.update_children(module.location, new_children) # save metadata modulestore.update_metadata(module.location, module._model_data._kvs._metadata) # now iterate through all of the assets and clone them # first the thumbnails thumbs = contentstore.get_all_content_thumbnails_for_course( source_location) for thumb in thumbs: thumb_loc = Location(thumb["_id"]) content = contentstore.find(thumb_loc) content.location = content.location._replace( org=dest_location.org, course=dest_location.course) print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location) contentstore.save(content) # now iterate through all of the assets, also updating the thumbnail pointer assets = contentstore.get_all_content_for_course(source_location) for asset in assets: asset_loc = Location(asset["_id"]) content = contentstore.find(asset_loc) content.location = content.location._replace( org=dest_location.org, course=dest_location.course) # be sure to update the pointer to the thumbnail if content.thumbnail_location is not None: content.thumbnail_location = content.thumbnail_location._replace( org=dest_location.org, course=dest_location.course) print "Cloning asset {0} to {1}".format(asset_loc, content.location) contentstore.save(content) return True
class OpenEndedChildTest(unittest.TestCase): """ Test the open ended child class """ location = Location( ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) metadata = json.dumps({'attempts': '10'}) prompt = etree.XML("<prompt>This is a question prompt</prompt>") rubric = '''<rubric><rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>Second option</option> </category> </rubric></rubric>''' max_score = 1 static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': None, 's3_interface': "", 'open_ended_grading_interface': {}, 'skip_basic_checks': False, 'control': { 'required_peer_grading': 1, 'peer_grader_count': 1, 'min_to_calibrate': 3, 'max_to_calibrate': 6, 'peer_grade_finished_submissions_when_none_pending': False, } } definition = Mock() descriptor = Mock() def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.openendedchild = OpenEndedChild(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_latest_answer_empty(self): answer = self.openendedchild.latest_answer() self.assertEqual(answer, "") def test_latest_score_empty(self): answer = self.openendedchild.latest_score() self.assertEqual(answer, None) def test_latest_post_assessment_empty(self): answer = self.openendedchild.latest_post_assessment(self.test_system) self.assertEqual(answer, "") def test_new_history_entry(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) answer = self.openendedchild.latest_answer() self.assertEqual(answer, new_answer) new_answer = "Newer Answer" self.openendedchild.new_history_entry(new_answer) answer = self.openendedchild.latest_answer() self.assertEqual(new_answer, answer) def test_record_latest_score(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) new_score = 3 self.openendedchild.record_latest_score(new_score) score = self.openendedchild.latest_score() self.assertEqual(score, 3) new_score = 4 self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(new_score) score = self.openendedchild.latest_score() self.assertEqual(score, 4) def test_record_latest_post_assessment(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) post_assessment = "Post assessment" self.openendedchild.record_latest_post_assessment(post_assessment) self.assertEqual( post_assessment, self.openendedchild.latest_post_assessment(self.test_system)) def test_get_score(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) score = self.openendedchild.get_score() self.assertEqual(score['score'], 0) self.assertEqual(score['total'], self.static_data['max_score']) new_score = 4 self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(new_score) score = self.openendedchild.get_score() self.assertEqual(score['score'], new_score) self.assertEqual(score['total'], self.static_data['max_score']) def test_reset(self): self.openendedchild.reset(self.test_system) state = json.loads(self.openendedchild.get_instance_state()) self.assertEqual(state['child_state'], OpenEndedChild.INITIAL) def test_is_last_response_correct(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(self.static_data['max_score']) self.assertEqual(self.openendedchild.is_last_response_correct(), 'correct') self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(0) self.assertEqual(self.openendedchild.is_last_response_correct(), 'incorrect')
def test_translate_location_read_only(self): """ Test the variants of translate_location which don't create entries, just decode """ # lookup before there are any maps org = 'foo_org' course = 'bar_course' old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run') with self.assertRaises(ItemNotFoundError): _ = loc_mapper().translate_location(old_style_course_id, Location( 'i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) new_style_package_id = '{}.geek_dept.{}.baz_run'.format(org, course) block_map = { 'abc123': { 'problem': 'problem2', 'vertical': 'vertical2' }, 'def456': { 'problem': 'problem4' }, 'ghi789': { 'problem': 'problem7' }, } loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'baz_run'), new_style_package_id, block_map=block_map) test_problem_locn = Location('i4x', org, course, 'problem', 'abc123') # only one course matches # look for w/ only the Location (works b/c there's only one possible course match). Will force # cache as default translation for this problemid self.translate_n_check(test_problem_locn, None, new_style_package_id, 'problem2', 'published') # look for non-existent problem with self.assertRaises(ItemNotFoundError): loc_mapper().translate_location(None, Location('i4x', org, course, 'problem', '1def23'), add_entry_if_missing=False) test_no_cat_locn = test_problem_locn.replace(category=None) with self.assertRaises(InvalidLocationError): loc_mapper().translate_location(old_style_course_id, test_no_cat_locn, False, False) test_no_cat_locn = test_no_cat_locn.replace(name='def456') # only one course matches self.translate_n_check(test_no_cat_locn, old_style_course_id, new_style_package_id, 'problem4', 'published') # add a distractor course (note that abc123 has a different translation in this one) distractor_block_map = { 'abc123': { 'problem': 'problem3' }, 'def456': { 'problem': 'problem4' }, 'ghi789': { 'problem': 'problem7' }, } test_delta_new_id = '{}.geek_dept.{}.{}'.format( org, course, 'delta_run') test_delta_old_id = '{}/{}/{}'.format(org, course, 'delta_run') loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'delta_run'), test_delta_new_id, block_map=distractor_block_map) # test that old translation still works self.translate_n_check(test_problem_locn, old_style_course_id, new_style_package_id, 'problem2', 'published') # and new returns new id self.translate_n_check(test_problem_locn, test_delta_old_id, test_delta_new_id, 'problem3', 'published') # look for default translation of uncached Location (not unique; so, just verify it returns something) prob_locator = loc_mapper().translate_location( None, Location('i4x', org, course, 'problem', 'def456'), add_entry_if_missing=False) self.assertIsNotNone(prob_locator, "couldn't find ambiguous location") # make delta_run default course: anything not cached using None as old_course_id will use this loc_mapper().create_map_entry(Location('i4x', org, course, 'problem', '789abc123efg456'), test_delta_new_id, block_map=block_map) # now an uncached ambiguous query should return delta test_unused_locn = Location('i4x', org, course, 'problem', 'ghi789') self.translate_n_check(test_unused_locn, None, test_delta_new_id, 'problem7', 'published') # get the draft one (I'm sorry this is getting long) self.translate_n_check(test_unused_locn, None, test_delta_new_id, 'problem7', 'draft')
class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): """ Test the student flow in the combined open ended xmodule """ problem_location = Location( ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) answer = "blah blah" assessment = [0, 1] hint = "blah" def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.test_system.xqueue['interface'] = Mock(send_to_queue=Mock( side_effect=[1, "queued"])) self.setup_modulestore(COURSE) def test_open_ended_load_and_save(self): """ See if we can load the module and save an answer @return: """ # Load the module module = self.get_module_from_location(self.problem_location, COURSE) # Try saving an answer module.handle_ajax("save_answer", {"student_answer": self.answer}) # Save our modifications to the underlying KeyValueStore so they can be persisted module.save() task_one_json = json.loads(module.task_states[0]) self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer) module = self.get_module_from_location(self.problem_location, COURSE) task_one_json = json.loads(module.task_states[0]) self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer) def test_open_ended_flow_reset(self): """ Test the flow of the module if we complete the self assessment step and then reset @return: """ assessment = [0, 1] module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer html = module.handle_ajax("get_html", {}) module.handle_ajax("save_answer", {"student_answer": self.answer}) html = module.handle_ajax("get_html", {}) #Mock a student submitting an assessment assessment_dict = MockQueryDict() assessment_dict.update({ 'assessment': sum(assessment), 'score_list[]': assessment }) module.handle_ajax("save_assessment", assessment_dict) task_one_json = json.loads(module.task_states[0]) self.assertEqual( json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) rubric = module.handle_ajax("get_combined_rubric", {}) #Move to the next step in the problem module.handle_ajax("next_problem", {}) self.assertEqual(module.current_task_number, 0) html = module.get_html() self.assertTrue(isinstance(html, basestring)) rubric = module.handle_ajax("get_combined_rubric", {}) self.assertTrue(isinstance(rubric, basestring)) self.assertEqual(module.state, "assessing") module.handle_ajax("reset", {}) self.assertEqual(module.current_task_number, 0) def test_open_ended_flow_correct(self): """ Test a two step problem where the student first goes through the self assessment step, and then the open ended step. @return: """ assessment = [1, 1] #Load the module module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer module.handle_ajax("save_answer", {"student_answer": self.answer}) status = module.handle_ajax("get_status", {}) self.assertTrue(isinstance(status, basestring)) #Mock a student submitting an assessment assessment_dict = MockQueryDict() assessment_dict.update({ 'assessment': sum(assessment), 'score_list[]': assessment }) module.handle_ajax("save_assessment", assessment_dict) task_one_json = json.loads(module.task_states[0]) self.assertEqual( json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) #Move to the next step in the problem try: module.handle_ajax("next_problem", {}) except GradingServiceError: #This error is okay. We don't have a grading service to connect to! pass self.assertEqual(module.current_task_number, 1) try: module.get_html() except GradingServiceError: #This error is okay. We don't have a grading service to connect to! pass #Try to get the rubric from the module module.handle_ajax("get_combined_rubric", {}) #Make a fake reply from the queue queue_reply = { 'queuekey': "", 'xqueue_body': json.dumps({ 'score': 0, 'feedback': json.dumps({ "spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.", "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we <bg>have no books left</bg> on the shelf for any of us . <bs>katherine</bs> <bs>paterson</bs> , author write a persuasive essay to a newspaper reflecting your vies on censorship <bg>in libraries . do</bg> you believe that certain materials , such as books , music , movies , magazines , <bg>etc . , should be</bg> removed from the shelves if they are found <bg>offensive ? support your</bg> position with convincing arguments from your own experience , observations <bg>, and or reading .</bg> " }), 'grader_type': "ML", 'success': True, 'grader_id': 1, 'submission_id': 1, 'rubric_xml': "<rubric><category><description>Writing Applications</description><score>0</score><option points='0'> The essay loses focus, has little information or supporting details, and the organization makes it difficult to follow.</option><option points='1'> The essay presents a mostly unified theme, includes sufficient information to convey the theme, and is generally organized well.</option></category><category><description> Language Conventions </description><score>0</score><option points='0'> The essay demonstrates a reasonable command of proper spelling and grammar. </option><option points='1'> The essay demonstrates superior command of proper spelling and grammar.</option></category></rubric>", 'rubric_scores_complete': True, }) } module.handle_ajax("check_for_score", {}) #Update the module with the fake queue reply module.handle_ajax("score_update", queue_reply) self.assertFalse(module.ready_to_reset) self.assertEqual(module.current_task_number, 1) #Get html and other data client will request module.get_html() module.handle_ajax("skip_post_assessment", {}) #Get all results module.handle_ajax("get_combined_rubric", {}) #reset the problem module.handle_ajax("reset", {}) self.assertEqual(module.state, "initial")
def test_translate_location_dwim(self): """ Test the location translation mechanisms which try to do-what-i-mean by creating new entries for never seen queries. """ org = 'foo_org' course = 'bar_course' old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run') problem_name = 'abc123abc123abc123abc123abc123f9' location = Location('i4x', org, course, 'problem', problem_name) new_style_package_id = '{}.{}.{}'.format(org, course, 'baz_run') self.translate_n_check(location, old_style_course_id, new_style_package_id, 'problemabc', 'published', True) # look for w/ only the Location (works b/c there's only one possible course match): causes cache self.translate_n_check(location, None, new_style_package_id, 'problemabc', 'published', True) # create an entry w/o a guid name other_location = Location('i4x', org, course, 'chapter', 'intro') self.translate_n_check(other_location, old_style_course_id, new_style_package_id, 'intro', 'published', True) # add a distractor course delta_new_package_id = '{}.geek_dept.{}.{}'.format( org, course, 'delta_run') delta_course_locn = Location('i4x', org, course, 'course', 'delta_run') loc_mapper().create_map_entry( delta_course_locn, delta_new_package_id, block_map={problem_name: { 'problem': 'problem3' }}) self.translate_n_check(location, old_style_course_id, new_style_package_id, 'problemabc', 'published', True) # add a new one to both courses (ensure name doesn't have same beginning) new_prob_name = uuid.uuid4().hex while new_prob_name.startswith('abc'): new_prob_name = uuid.uuid4().hex new_prob_locn = location.replace(name=new_prob_name) new_usage_id = 'problem{}'.format(new_prob_name[:3]) self.translate_n_check(new_prob_locn, old_style_course_id, new_style_package_id, new_usage_id, 'published', True) self.translate_n_check(new_prob_locn, delta_course_locn.course_id, delta_new_package_id, new_usage_id, 'published', True) # look for w/ only the Location: causes caching and not unique; so, can't check which course prob_locator = loc_mapper().translate_location( None, new_prob_locn, add_entry_if_missing=True) self.assertIsNotNone(prob_locator, "couldn't find ambiguous location") # add a default course pointing to the delta_run loc_mapper().create_map_entry( Location('i4x', org, course, 'problem', '789abc123efg456'), delta_new_package_id, block_map={problem_name: { 'problem': 'problem3' }}) # now the ambiguous query should return delta again_prob_name = uuid.uuid4().hex while again_prob_name.startswith('abc') or again_prob_name.startswith( new_prob_name[:3]): again_prob_name = uuid.uuid4().hex again_prob_locn = location.replace(name=again_prob_name) again_usage_id = 'problem{}'.format(again_prob_name[:3]) self.translate_n_check(again_prob_locn, old_style_course_id, new_style_package_id, again_usage_id, 'published', True) self.translate_n_check(again_prob_locn, delta_course_locn.course_id, delta_new_package_id, again_usage_id, 'published', True) self.translate_n_check(again_prob_locn, None, delta_new_package_id, again_usage_id, 'published', True)
def _create(cls, target_class, **kwargs): """ Uses ``**kwargs``: :parent_location: (required): the location of the parent module (e.g. the parent course or section) :category: the category of the resulting item. :data: (optional): the data for the item (e.g. XML problem definition for a problem item) :display_name: (optional): the display name of the item :metadata: (optional): dictionary of metadata attributes :boilerplate: (optional) the boilerplate for overriding field values :target_class: is ignored """ # All class attributes (from this class and base classes) are # passed in via **kwargs. However, some of those aren't actual field values, # so pop those off for use separately DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] # catch any old style users before they get into trouble assert 'template' not in kwargs parent_location = Location(kwargs.pop('parent_location', None)) data = kwargs.pop('data', None) category = kwargs.pop('category', None) display_name = kwargs.pop('display_name', None) metadata = kwargs.pop('metadata', {}) location = kwargs.pop('location') assert location != parent_location store = kwargs.pop('modulestore') # This code was based off that in cms/djangoapps/contentstore/views.py parent = kwargs.pop('parent', None) or store.get_item(parent_location) if 'boilerplate' in kwargs: template_id = kwargs.pop('boilerplate') clz = XBlock.load_class(category, select=prefer_xmodules) template = clz.get_template(template_id) assert template is not None metadata.update(template.get('metadata', {})) if not isinstance(data, basestring): data.update(template.get('data')) # replace the display name with an optional parameter passed in from the caller if display_name is not None: metadata['display_name'] = display_name module = store.create_and_save_xmodule(location, metadata=metadata, definition_data=data) module = store.get_item(location) for attr, val in kwargs.items(): setattr(module, attr, val) module.save() store.save_xmodule(module) if location.category not in DETACHED_CATEGORIES: parent.children.append(location.url()) store.update_children(parent_location, parent.children) return store.get_item(location)
def test_translate_locator(self): """ tests translate_locator_to_location(BlockUsageLocator) """ # lookup for non-existent course org = 'foo_org' course = 'bar_course' new_style_package_id = '{}.geek_dept.{}.baz_run'.format(org, course) prob_locator = BlockUsageLocator(package_id=new_style_package_id, block_id='problem2', branch='published') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertIsNone(prob_location, 'found entry in empty map table') loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'baz_run'), new_style_package_id, block_map={ 'abc123': { 'problem': 'problem2' }, '48f23a10395384929234': { 'chapter': 'chapter48f' }, 'baz_run': { 'course': 'root' }, }) # only one course matches prob_location = loc_mapper().translate_locator_to_location( prob_locator) # default branch self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) # test get_course keyword prob_location = loc_mapper().translate_locator_to_location( prob_locator, get_course=True) self.assertEqual( prob_location, Location('i4x', org, course, 'course', 'baz_run', None)) # explicit branch prob_locator = BlockUsageLocator(package_id=prob_locator.package_id, branch='draft', block_id=prob_locator.block_id) prob_location = loc_mapper().translate_locator_to_location( prob_locator) # Even though the problem was set as draft, we always return revision=None to work # with old mongo/draft modulestores. self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) prob_locator = BlockUsageLocator(package_id=new_style_package_id, block_id='problem2', branch='production') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) # same for chapter except chapter cannot be draft in old system chap_locator = BlockUsageLocator(package_id=new_style_package_id, block_id='chapter48f', branch='production') chap_location = loc_mapper().translate_locator_to_location( chap_locator) self.assertEqual( chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234')) # explicit branch chap_locator.branch = 'draft' chap_location = loc_mapper().translate_locator_to_location( chap_locator) self.assertEqual( chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234')) chap_locator = BlockUsageLocator(package_id=new_style_package_id, block_id='chapter48f', branch='production') chap_location = loc_mapper().translate_locator_to_location( chap_locator) self.assertEqual( chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234')) # look for non-existent problem prob_locator2 = BlockUsageLocator(package_id=new_style_package_id, branch='draft', block_id='problem3') prob_location = loc_mapper().translate_locator_to_location( prob_locator2) self.assertIsNone(prob_location, 'Found non-existent problem') # add a distractor course new_style_package_id = '{}.geek_dept.{}.{}'.format( org, course, 'delta_run') loc_mapper().create_map_entry( Location('i4x', org, course, 'course', 'delta_run'), new_style_package_id, block_map={'abc123': { 'problem': 'problem3' }}) prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) # add a default course pointing to the delta_run loc_mapper().create_map_entry( Location('i4x', org, course, 'problem', '789abc123efg456'), new_style_package_id, block_map={'abc123': { 'problem': 'problem3' }}) # now query delta (2 entries point to it) prob_locator = BlockUsageLocator(package_id=new_style_package_id, branch='production', block_id='problem3') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123'))
def test_none(): assert_equals([None] * 6, Location(None).list())
def test_location(self): input_list = ['tag', 'org', 'course', 'category', 'name'] self.assertEquals("tag://org/course/category/name", Location(Location(input_list)).url())
def test_html_id(): loc = Location("tag://org/course/cat/name:more_name@rev") assert_equals(loc.html_id(), "tag-org-course-cat-name_more_name-rev")
def from_xml(cls, xml_data, system, org=None, course=None): """ Creates an instance of this descriptor from the supplied xml_data. This may be overridden by subclasses xml_data: A string of xml that will be translated into data and children for this module system: A DescriptorSystem for interacting with external resources org and course are optional strings that will be used in the generated modules url identifiers """ xml_object = etree.fromstring(xml_data) # VS[compat] -- just have the url_name lookup, once translation is done url_name = xml_object.get('url_name', xml_object.get('slug')) location = Location('i4x', org, course, xml_object.tag, url_name) # VS[compat] -- detect new-style each-in-a-file mode if is_pointer_tag(xml_object): # new style: # read the actual definition file--named using url_name.replace(':','/') filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name)) definition_xml = cls.load_file(filepath, system.resources_fs, location) else: definition_xml = xml_object filepath = None definition, children = cls.load_definition( definition_xml, system, location) # note this removes metadata # VS[compat] -- make Ike's github preview links work in both old and # new file layouts if is_pointer_tag(xml_object): # new style -- contents actually at filepath definition['filename'] = [filepath, filepath] metadata = cls.load_metadata(definition_xml) # move definition metadata into dict dmdata = definition.get('definition_metadata', '') if dmdata: metadata['definition_metadata_raw'] = dmdata try: metadata.update(json.loads(dmdata)) except Exception as err: log.debug('Error %s in loading metadata %s' % (err, dmdata)) metadata['definition_metadata_err'] = str(err) # Set/override any metadata specified by policy k = policy_key(location) if k in system.policy: cls.apply_policy(metadata, system.policy[k]) field_data = {} field_data.update(metadata) field_data.update(definition) field_data['children'] = children field_data['xml_attributes'] = {} field_data['xml_attributes']['filename'] = definition.get( 'filename', ['', None]) # for git link for key, value in metadata.items(): if key not in cls.fields: field_data['xml_attributes'][key] = value field_data['location'] = location field_data['category'] = xml_object.tag kvs = InheritanceKeyValueStore(initial_values=field_data) field_data = DbModel(kvs) return system.construct_xblock_from_class( cls, field_data, # We're loading a descriptor, so student_id is meaningless # We also don't have separate notions of definition and usage ids yet, # so we use the location for both ScopeIds(None, location.category, location, location))