예제 #1
0
    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')
예제 #2
0
    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)
예제 #3
0
    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='
            '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
            'alt-with-value="&quot;Image&quot;"></oppia-noninteractive-image>')
        explanation_html_2 = (
            '<oppia-noninteractive-image filepath-with-value='
            '"&quot;img_2.png&quot;" caption-with-value="&quot;&quot;" '
            'alt-with-value="&quot;Image 2&quot;"></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()
예제 #5
0
    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)
예제 #6
0
    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)
예제 #7
0
    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()
예제 #10
0
    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())
예제 #11
0
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
예제 #12
0
    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])
예제 #13
0
 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)