def test_get_skill_by_id_with_different_versions(self): changelist = [ skill_domain.SkillChange({ 'cmd': skill_domain.CMD_UPDATE_SKILL_PROPERTY, 'property_name': skill_domain.SKILL_PROPERTY_LANGUAGE_CODE, 'old_value': 'en', 'new_value': 'bn' }) ] skill_services.update_skill(self.USER_ID, self.SKILL_ID, changelist, 'update language code') skill = skill_fetchers.get_skill_by_id(self.SKILL_ID, version=1) self.assertEqual(skill.id, self.SKILL_ID) self.assertEqual(skill.language_code, 'en') skill = skill_fetchers.get_skill_by_id(self.SKILL_ID, version=2) self.assertEqual(skill.id, self.SKILL_ID) self.assertEqual(skill.language_code, 'bn')
def map(item): if item.deleted: yield (SkillMigrationOneOffJob._DELETED_KEY, 1) return # Note: the read will bring the skill up to the newest version. skill = skill_fetchers.get_skill_by_id(item.id) try: skill.validate() except Exception as e: logging.exception( 'Skill %s failed validation: %s' % (item.id, e)) yield ( SkillMigrationOneOffJob._ERROR_KEY, 'Skill %s failed validation: %s' % (item.id, e)) return # Write the new skill into the datastore if it's different from # the old version. commit_cmds = [] if ( item.skill_contents_schema_version <= feconf.CURRENT_SKILL_CONTENTS_SCHEMA_VERSION): commit_cmds.append(skill_domain.SkillChange({ 'cmd': skill_domain.CMD_MIGRATE_CONTENTS_SCHEMA_TO_LATEST_VERSION, # pylint: disable=line-too-long 'from_version': item.skill_contents_schema_version, 'to_version': feconf.CURRENT_SKILL_CONTENTS_SCHEMA_VERSION })) if ( item.misconceptions_schema_version <= feconf.CURRENT_MISCONCEPTIONS_SCHEMA_VERSION): commit_cmds.append(skill_domain.SkillChange({ 'cmd': skill_domain.CMD_MIGRATE_MISCONCEPTIONS_SCHEMA_TO_LATEST_VERSION, # pylint: disable=line-too-long 'from_version': item.misconceptions_schema_version, 'to_version': feconf.CURRENT_MISCONCEPTIONS_SCHEMA_VERSION })) if ( item.rubric_schema_version <= feconf.CURRENT_RUBRIC_SCHEMA_VERSION): commit_cmds.append(skill_domain.SkillChange({ 'cmd': skill_domain.CMD_MIGRATE_RUBRICS_SCHEMA_TO_LATEST_VERSION, # pylint: disable=line-too-long 'from_version': item.rubric_schema_version, 'to_version': feconf.CURRENT_RUBRIC_SCHEMA_VERSION })) if commit_cmds: skill_services.update_skill( feconf.MIGRATION_BOT_USERNAME, item.id, commit_cmds, 'Update skill content schema version to %d and ' 'skill misconceptions schema version to %d and ' 'skill rubrics schema version to %d.' % ( feconf.CURRENT_SKILL_CONTENTS_SCHEMA_VERSION, feconf.CURRENT_MISCONCEPTIONS_SCHEMA_VERSION, feconf.CURRENT_RUBRIC_SCHEMA_VERSION)) yield (SkillMigrationOneOffJob._MIGRATED_KEY, 1)
def get(self, skill_id): """Handles GET requests.""" skill_domain.Skill.require_valid_skill_id(skill_id) skill = skill_fetchers.get_skill_by_id(skill_id, strict=False) if skill is None: raise self.PageNotFoundException( Exception('The skill with the given id doesn\'t exist.')) self.render_template('skill-editor-page.mainpage.html')
def test_skill_creation_with_valid_images(self): self.login(self.CURRICULUM_ADMIN_EMAIL) csrf_token = self.get_new_csrf_token() filename = 'img.png' filename_2 = 'img_2.png' explanation_html = ( '<oppia-noninteractive-image filepath-with-value=' '""img.png"" caption-with-value="""" ' 'alt-with-value=""Image""></oppia-noninteractive-image>') explanation_html_2 = ( '<oppia-noninteractive-image filepath-with-value=' '""img_2.png"" caption-with-value="""" ' 'alt-with-value=""Image 2""></oppia-noninteractive-image>' ) rubrics = [{ 'difficulty': constants.SKILL_DIFFICULTIES[0], 'explanations': ['Explanation 1', explanation_html_2] }, { 'difficulty': constants.SKILL_DIFFICULTIES[1], 'explanations': ['Explanation 2'] }, { 'difficulty': constants.SKILL_DIFFICULTIES[2], 'explanations': ['Explanation 3'] }] post_data = { 'description': 'Skill Description', 'rubrics': rubrics, 'explanation_dict': state_domain.SubtitledHtml('1', explanation_html).to_dict(), 'thumbnail_filename': 'image.svg' } with python_utils.open_file(os.path.join(feconf.TESTS_DATA_DIR, 'img.png'), 'rb', encoding=None) as f: raw_image = f.read() json_response = self.post_json(self.url, post_data, csrf_token=csrf_token, upload_files=( (filename, filename, raw_image), (filename_2, filename_2, raw_image), )) skill_id = json_response['skillId'] self.assertIsNotNone( skill_fetchers.get_skill_by_id(skill_id, strict=False)) self.logout()
def get(self, skill_id): """Populates the data on the individual skill page.""" try: skill_domain.Skill.require_valid_skill_id(skill_id) except utils.ValidationError: raise self.PageNotFoundException('Invalid skill id.') skill = skill_fetchers.get_skill_by_id(skill_id, strict=False) if skill is None: raise self.PageNotFoundException( Exception('The skill with the given id doesn\'t exist.')) topics = topic_fetchers.get_all_topics() grouped_skill_summary_dicts = {} # It might be the case that the requested skill is not assigned to any # topic, or it might be assigned to a topic and not a subtopic, or # it can be assigned to multiple topics, so this dict represents # a key value pair where key is topic's name and value is subtopic # name which might be None indicating that the skill is assigned # to that topic but not to a subtopic. assigned_skill_topic_data_dict = {} for topic in topics: skill_ids_in_topic = topic.get_all_skill_ids() if skill_id in skill_ids_in_topic: subtopic_name = None for subtopic in topic.subtopics: if skill_id in subtopic.skill_ids: subtopic_name = subtopic.title break assigned_skill_topic_data_dict[topic.name] = subtopic_name skill_summaries = skill_services.get_multi_skill_summaries( skill_ids_in_topic) skill_summary_dicts = [ summary.to_dict() for summary in skill_summaries ] grouped_skill_summary_dicts[topic.name] = skill_summary_dicts self.values.update({ 'skill': skill.to_dict(), 'assigned_skill_topic_data_dict': assigned_skill_topic_data_dict, 'grouped_skill_summaries': grouped_skill_summary_dicts }) self.render_json(self.values)
def put(self, skill_id): """Updates properties of the given skill.""" skill_domain.Skill.require_valid_skill_id(skill_id) skill = skill_fetchers.get_skill_by_id(skill_id, strict=False) if skill is None: raise self.PageNotFoundException( Exception('The skill with the given id doesn\'t exist.')) version = self.payload.get('version') _require_valid_version(version, skill.version) commit_message = self.payload.get('commit_message') if (commit_message is not None and len(commit_message) > constants.MAX_COMMIT_MESSAGE_LENGTH): raise self.InvalidInputException( 'Commit messages must be at most %s characters long.' % constants.MAX_COMMIT_MESSAGE_LENGTH) change_dicts = self.payload.get('change_dicts') change_list = [ skill_domain.SkillChange(change_dict) for change_dict in change_dicts ] try: skill_services.update_skill( self.user_id, skill_id, change_list, commit_message) except utils.ValidationError as e: raise self.InvalidInputException(e) skill_dict = skill_fetchers.get_skill_by_id(skill_id).to_dict() self.values.update({ 'skill': skill_dict }) self.render_json(self.values)
def pre_accept_validate(self): """Performs referential validation. This function needs to be called before accepting the suggestion. """ if self.change.skill_id is None: raise utils.ValidationError('Expected change to contain skill_id') question_dict = self.change.question_dict self.validate() if (question_dict['question_state_data_schema_version'] != feconf.CURRENT_STATE_SCHEMA_VERSION): raise utils.ValidationError( 'Question state schema version is not up to date.') skill_domain.Skill.require_valid_skill_id(self.change.skill_id) skill = skill_fetchers.get_skill_by_id(self.change.skill_id, strict=False) if skill is None: raise utils.ValidationError( 'The skill with the given id doesn\'t exist.')
def test_migration_job_does_not_convert_up_to_date_skill(self): """Tests that the skill migration job does not convert a skill that is already the latest schema version. """ # Create a new skill that should not be affected by the # job. skill = skill_domain.Skill.create_default_skill( self.SKILL_ID, 'A description', self.rubrics) skill_services.save_new_skill(self.albert_id, skill) self.assertEqual( skill.skill_contents_schema_version, feconf.CURRENT_SKILL_CONTENTS_SCHEMA_VERSION) self.assertEqual( skill.misconceptions_schema_version, feconf.CURRENT_MISCONCEPTIONS_SCHEMA_VERSION) self.assertEqual( skill.rubric_schema_version, feconf.CURRENT_RUBRIC_SCHEMA_VERSION) # Start migration job. job_id = ( skill_jobs_one_off.SkillMigrationOneOffJob.create_new()) skill_jobs_one_off.SkillMigrationOneOffJob.enqueue(job_id) self.process_and_flush_pending_mapreduce_tasks() # Verify the skill is exactly the same after migration. updated_skill = ( skill_fetchers.get_skill_by_id(self.SKILL_ID)) self.assertEqual( updated_skill.skill_contents_schema_version, feconf.CURRENT_SKILL_CONTENTS_SCHEMA_VERSION) self.assertEqual( updated_skill.misconceptions_schema_version, feconf.CURRENT_MISCONCEPTIONS_SCHEMA_VERSION) self.assertEqual( updated_skill.rubric_schema_version, feconf.CURRENT_RUBRIC_SCHEMA_VERSION) output = skill_jobs_one_off.SkillMigrationOneOffJob.get_output(job_id) expected = [[u'skill_migrated', [u'1 skills successfully migrated.']]] self.assertEqual(expected, [ast.literal_eval(x) for x in output])
def test_skill_creation_in_valid_topic(self): self.login(self.CURRICULUM_ADMIN_EMAIL) csrf_token = self.get_new_csrf_token() rubrics = [{ 'difficulty': constants.SKILL_DIFFICULTIES[0], 'explanations': ['Explanation 1'] }, { 'difficulty': constants.SKILL_DIFFICULTIES[1], 'explanations': ['Explanation 2'] }, { 'difficulty': constants.SKILL_DIFFICULTIES[2], 'explanations': ['Explanation 3'] }] payload = { 'description': 'Skill Description', 'linked_topic_ids': [self.topic_id], 'rubrics': rubrics, 'explanation_dict': state_domain.SubtitledHtml('1', '<p>Explanation</p>').to_dict(), 'thumbnail_filename': 'image.svg' } json_response = self.post_json( self.url, payload, csrf_token=csrf_token, upload_files=(('image', 'unused_filename', self.original_image_content), )) skill_id = json_response['skillId'] self.assertEqual(len(skill_id), 12) self.assertIsNotNone( skill_fetchers.get_skill_by_id(skill_id, strict=False)) topic = topic_fetchers.get_topic_by_id(self.topic_id) self.assertEqual(topic.uncategorized_skill_ids, [self.linked_skill_id, skill_id]) self.logout()
def accept(self, unused_commit_message): """Accepts the suggestion. Args: unused_commit_message: str. This parameter is passed in for consistency with the existing suggestions. As a default commit message is used in the add_question function, the arg is unused. """ question_dict = self.change.question_dict question_dict['version'] = 1 question_dict['id'] = (question_services.get_new_question_id()) question_dict['linked_skill_ids'] = [self.change.skill_id] question = question_domain.Question.from_dict(question_dict) question.validate() question_services.add_question(self.author_id, question) skill = skill_fetchers.get_skill_by_id(self.change.skill_id, strict=False) if skill is None: raise utils.ValidationError( 'The skill with the given id doesn\'t exist.') question_services.create_new_question_skill_link( self.author_id, question_dict['id'], self.change.skill_id, self._get_skill_difficulty())
def apply_change_list(skill_id, change_list, committer_id): """Applies a changelist to a skill and returns the result. Args: skill_id: str. ID of the given skill. change_list: list(SkillChange). A change list to be applied to the given skill. committer_id: str. The ID of the committer of this change list. Returns: Skill. The resulting skill domain object. Raises: Exception. The user does not have enough rights to edit the skill description. Exception. Invalid change dict. """ skill = skill_fetchers.get_skill_by_id(skill_id) user = user_services.get_user_actions_info(committer_id) try: for change in change_list: if change.cmd == skill_domain.CMD_UPDATE_SKILL_PROPERTY: if (change.property_name == skill_domain.SKILL_PROPERTY_DESCRIPTION): if role_services.ACTION_EDIT_SKILL_DESCRIPTION not in ( user.actions): raise Exception( 'The user does not have enough rights to edit the ' 'skill description.') skill.update_description(change.new_value) ( opportunity_services .update_skill_opportunity_skill_description( skill.id, change.new_value)) elif (change.property_name == skill_domain.SKILL_PROPERTY_LANGUAGE_CODE): skill.update_language_code(change.new_value) elif (change.property_name == skill_domain.SKILL_PROPERTY_SUPERSEDING_SKILL_ID): skill.update_superseding_skill_id(change.new_value) elif (change.property_name == skill_domain.SKILL_PROPERTY_ALL_QUESTIONS_MERGED): skill.record_that_all_questions_are_merged(change.new_value) elif change.cmd == skill_domain.CMD_UPDATE_SKILL_CONTENTS_PROPERTY: if (change.property_name == skill_domain.SKILL_CONTENTS_PROPERTY_EXPLANATION): explanation = ( state_domain.SubtitledHtml.from_dict(change.new_value)) explanation.validate() skill.update_explanation(explanation) elif (change.property_name == skill_domain.SKILL_CONTENTS_PROPERTY_WORKED_EXAMPLES): worked_examples_list = [ skill_domain.WorkedExample.from_dict(worked_example) for worked_example in change.new_value] skill.update_worked_examples(worked_examples_list) elif change.cmd == skill_domain.CMD_ADD_SKILL_MISCONCEPTION: misconception = skill_domain.Misconception.from_dict( change.new_misconception_dict) skill.add_misconception(misconception) elif change.cmd == skill_domain.CMD_DELETE_SKILL_MISCONCEPTION: skill.delete_misconception(change.misconception_id) elif change.cmd == skill_domain.CMD_ADD_PREREQUISITE_SKILL: skill.add_prerequisite_skill(change.skill_id) elif change.cmd == skill_domain.CMD_DELETE_PREREQUISITE_SKILL: skill.delete_prerequisite_skill(change.skill_id) elif change.cmd == skill_domain.CMD_UPDATE_RUBRICS: skill.update_rubric( change.difficulty, change.explanations) elif (change.cmd == skill_domain.CMD_UPDATE_SKILL_MISCONCEPTIONS_PROPERTY): if (change.property_name == skill_domain.SKILL_MISCONCEPTIONS_PROPERTY_NAME): skill.update_misconception_name( change.misconception_id, change.new_value) elif (change.property_name == skill_domain.SKILL_MISCONCEPTIONS_PROPERTY_NOTES): skill.update_misconception_notes( change.misconception_id, change.new_value) elif (change.property_name == skill_domain.SKILL_MISCONCEPTIONS_PROPERTY_FEEDBACK): skill.update_misconception_feedback( change.misconception_id, change.new_value) elif (change.property_name == skill_domain.SKILL_MISCONCEPTIONS_PROPERTY_MUST_BE_ADDRESSED): # pylint: disable=line-too-long skill.update_misconception_must_be_addressed( change.misconception_id, change.new_value) else: raise Exception('Invalid change dict.') elif (change.cmd in ( skill_domain.CMD_MIGRATE_CONTENTS_SCHEMA_TO_LATEST_VERSION, skill_domain.CMD_MIGRATE_MISCONCEPTIONS_SCHEMA_TO_LATEST_VERSION, # pylint: disable=line-too-long skill_domain.CMD_MIGRATE_RUBRICS_SCHEMA_TO_LATEST_VERSION )): # Loading the skill model from the datastore into a # skill domain object automatically converts it to use the # latest schema version. As a result, simply resaving the # skill is sufficient to apply the schema migration. continue return skill except Exception as e: logging.error( '%s %s %s %s' % ( e.__class__.__name__, e, skill_id, change_list) ) raise e
def test_migration_job_converts_old_skill(self): """Tests that the schema conversion functions work correctly and an old skill is converted to new version. """ # Generate skill with old(v1) misconceptions schema # version and old(v1) skill contents schema version. skill_contents = { 'worked_examples': [], 'explanation': { 'content_id': 'explanation', 'html': feconf.DEFAULT_SKILL_EXPLANATION }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'explanation': {} } }, 'written_translations': { 'translations_mapping': { 'explanation': {} } } } rubrics = [{ 'difficulty': 'Easy', 'explanation': 'easy explanation' }, { 'difficulty': 'Medium', 'explanation': 'medium explanation' }, { 'difficulty': 'Hard', 'explanation': 'hard explanation' }] self.save_new_skill_with_defined_schema_versions( self.SKILL_ID, self.albert_id, 'A description', 0, misconceptions=[], rubrics=rubrics, skill_contents=skill_contents, misconceptions_schema_version=1, skill_contents_schema_version=1, rubric_schema_version=1) # Start migration job. job_id = (skill_jobs_one_off.SkillMigrationOneOffJob.create_new()) skill_jobs_one_off.SkillMigrationOneOffJob.enqueue(job_id) self.process_and_flush_pending_mapreduce_tasks() # Verify that the skill migrates correctly. updated_skill = (skill_fetchers.get_skill_by_id(self.SKILL_ID)) self.assertEqual(updated_skill.skill_contents_schema_version, feconf.CURRENT_SKILL_CONTENTS_SCHEMA_VERSION) self.assertEqual(updated_skill.misconceptions_schema_version, feconf.CURRENT_MISCONCEPTIONS_SCHEMA_VERSION) self.assertEqual(updated_skill.rubric_schema_version, feconf.CURRENT_RUBRIC_SCHEMA_VERSION) output = skill_jobs_one_off.SkillMigrationOneOffJob.get_output(job_id) expected = [[u'skill_migrated', [u'1 skills successfully migrated.']]] self.assertEqual(expected, [ast.literal_eval(x) for x in output])
def test_get_skill_by_id(self): expected_skill = self.skill.to_dict() skill = skill_fetchers.get_skill_by_id(self.SKILL_ID) self.assertEqual(skill.to_dict(), expected_skill)