예제 #1
0
파일: suggestion.py 프로젝트: oppia/oppia
def _upload_suggestion_images(files, suggestion, filenames):
    """Saves a suggestion's images to storage.

    Args:
        files: dict. Files containing a mapping of image
            filename to image blob.
        suggestion: BaseSuggestion. The suggestion for which images are being
            uploaded.
        filenames: list(str). The image filenames.
    """
    suggestion_image_context = suggestion.image_context
    # TODO(#10513): Find a way to save the images before the suggestion is
    # created.
    for filename in filenames:
        image = files.get(filename)
        image = base64.decodebytes(image.encode('utf-8'))
        file_format = (image_validation_services.validate_image_and_filename(
            image, filename))
        image_is_compressible = (file_format
                                 in feconf.COMPRESSIBLE_IMAGE_FORMATS)
        fs_services.save_original_and_compressed_versions_of_image(
            filename, suggestion_image_context, suggestion.target_id, image,
            'image', image_is_compressible)

    target_entity_html_list = suggestion.get_target_entity_html_strings()
    target_image_filenames = (
        html_cleaner.get_image_filenames_from_html_strings(
            target_entity_html_list))

    fs_services.copy_images(suggestion.target_type, suggestion.target_id,
                            suggestion_image_context, suggestion.target_id,
                            target_image_filenames)
예제 #2
0
 def test_get_image_filenames_from_html_strings(self):
     html_strings = [
         '<oppia-noninteractive-image '
         'filepath-with-value="&quot;img.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image><oppia-noninteractive-image '
         'filepath-with-value="&quot;img2.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image>', '<oppia-noninteractive-image '
         'filepath-with-value="&quot;img3.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image><oppia-noninteractive-image '
         'filepath-with-value="&quot;img4.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image>', '<oppia-noninteractive-image '
         'filepath-with-value="&quot;img5.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image>'
         '<oppia-noninteractive-math math_content-with-value="{&amp;quo'
         't;raw_latex&amp;quot;:&amp;quot;+,-,-,+&amp;quot;,&amp;quot;sv'
         'g_filename&amp;quot;:&amp;quot;math1.svg&amp;quot;}"></oppia-n'
         'oninteractive-math>'
         '<oppia-noninteractive-math math_content-with-value="{&amp;quo'
         't;raw_latex&amp;quot;:&amp;quot;x^2&amp;quot;,&amp;quot;sv'
         'g_filename&amp;quot;:&amp;quot;math2.svg&amp;quot;}"></oppia-n'
         'oninteractive-math>'
         '<oppia-noninteractive-math math_content-with-value="{&amp;quo'
         't;raw_latex&amp;quot;:&amp;quot;(x-1)(x-2)^2&amp;quot;,&amp;quot'
         ';svg_filename&amp;quot;:&amp;quot;math3.svg&amp;quot;}"></oppia-n'
         'oninteractive-math>'
     ]
     self.assertItemsEqual([
         'img.svg', 'img2.svg', 'img3.svg', 'img4.svg', 'img5.svg',
         'math1.svg', 'math2.svg', 'math3.svg'
     ], html_cleaner.get_image_filenames_from_html_strings(html_strings))
예제 #3
0
 def test_get_image_filenames_from_html_strings(self):
     html_strings = [
         '<oppia-noninteractive-image '
         'filepath-with-value="&quot;img.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image><oppia-noninteractive-image '
         'filepath-with-value="&quot;img2.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image>', '<oppia-noninteractive-image '
         'filepath-with-value="&quot;img3.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image><oppia-noninteractive-image '
         'filepath-with-value="&quot;img4.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image>',
         '<oppia-noninteractive-svgdiagram '
         'svg_filename-with-value="&quot;img5.svg&quot;"'
         ' alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-svgdiagram><oppia-noninteractive-svgdiag'
         'ram svg_filename-with-value="&quot;img6.svg&quot;"'
         ' alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-svgdiagram><oppia-noninteractive-image '
         'filepath-with-value="&quot;img7.svg&quot;" caption-with-value='
         '"&quot;&quot;" alt-with-value="&quot;Image&quot;">'
         '</oppia-noninteractive-image>'
     ]
     self.assertItemsEqual([
         'img.svg', 'img2.svg', 'img3.svg', 'img4.svg', 'img5.svg',
         'img6.svg', 'img7.svg'
     ], html_cleaner.get_image_filenames_from_html_strings(html_strings))
예제 #4
0
    def map(item):
        if item.deleted:
            yield (FixQuestionImagesStorageOneOffJob._DELETED_KEY, 1)
            return

        question = question_fetchers.get_question_from_model(item)
        html_list = question.question_state_data.get_all_html_content_strings()
        image_filenames = html_cleaner.get_image_filenames_from_html_strings(
            html_list)
        file_system_class = fs_services.get_entity_file_system_class()
        question_fs = fs_domain.AbstractFileSystem(file_system_class(
            feconf.ENTITY_TYPE_QUESTION, question.id))
        success_count = 0
        # For each image filename, check if it exists in the correct path. If
        # not, copy the image file to the correct path else continue.
        for image_filename in image_filenames:
            if not question_fs.isfile('image/%s' % image_filename):
                for skill_id in question.linked_skill_ids:
                    skill_fs = fs_domain.AbstractFileSystem(file_system_class(
                        feconf.ENTITY_TYPE_SKILL, skill_id))
                    if skill_fs.isfile('image/%s' % image_filename):
                        fs_services.copy_images(
                            feconf.ENTITY_TYPE_SKILL, skill_id,
                            feconf.ENTITY_TYPE_QUESTION, question.id,
                            [image_filename])
                        success_count += 1
                        break
        if success_count > 0:
            yield (
                FixQuestionImagesStorageOneOffJob._IMAGE_COPIED,
                '%s image paths were fixed for question id %s with '
                'linked_skill_ids: %r' % (
                    success_count, question.id, question.linked_skill_ids))
예제 #5
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())
        html_list = self.get_all_html_content_strings()
        filenames = (
            html_cleaner.get_image_filenames_from_html_strings(html_list))
        image_context = fs_services.get_image_context_for_suggestion_target(
            self.target_type)
        fs_services.copy_images(image_context, self.target_id,
                                feconf.ENTITY_TYPE_QUESTION, self.target_id,
                                filenames)

        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())
예제 #6
0
    def post(self):
        try:
            # The create_suggestion method needs to be run in transaction as it
            # generates multiple connected models (suggestion, feedback thread,
            # feedback message etc.) and all these models needs to be created
            # together, in a batch.
            suggestion = transaction_services.run_in_transaction(
                suggestion_services.create_suggestion,
                self.payload.get('suggestion_type'),
                self.payload.get('target_type'), self.payload.get('target_id'),
                self.payload.get('target_version_at_submission'), self.user_id,
                self.payload.get('change'), self.payload.get('description'))
        except utils.ValidationError as e:
            raise self.InvalidInputException(e)

        # TODO(#10513) : Find a way to save the images before the suggestion is
        # created.
        suggestion_image_context = suggestion.image_context
        # For suggestion which doesn't need images for rendering the
        # image_context is set to None.
        if suggestion_image_context is None:
            self.render_json(self.values)
            return

        new_image_filenames = (
            suggestion.get_new_image_filenames_added_in_suggestion())
        for filename in new_image_filenames:
            image = self.request.get(filename)
            if not image:
                logging.error(
                    'Image not provided for file with name %s when the '
                    ' suggestion with target id %s was created.' %
                    (filename, suggestion.target_id))
                raise self.InvalidInputException(
                    'No image data provided for file with name %s.' %
                    (filename))
            try:
                file_format = (
                    image_validation_services.validate_image_and_filename(
                        image, filename))
            except utils.ValidationError as e:
                raise self.InvalidInputException('%s' % (e))
            image_is_compressible = (file_format
                                     in feconf.COMPRESSIBLE_IMAGE_FORMATS)
            fs_services.save_original_and_compressed_versions_of_image(
                filename, suggestion_image_context, suggestion.target_id,
                image, 'image', image_is_compressible)

        target_entity_html_list = suggestion.get_target_entity_html_strings()
        target_image_filenames = (
            html_cleaner.get_image_filenames_from_html_strings(
                target_entity_html_list))

        fs_services.copy_images(suggestion.target_type, suggestion.target_id,
                                suggestion_image_context, suggestion.target_id,
                                target_image_filenames)

        self.render_json(self.values)
예제 #7
0
    def post(self):
        """Handles POST requests."""
        if (self.payload.get('suggestion_type') ==
                feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT):
            raise self.InvalidInputException(
                'Content suggestion submissions are no longer supported.')

        try:
            suggestion = suggestion_services.create_suggestion(
                self.payload.get('suggestion_type'),
                self.payload.get('target_type'), self.payload.get('target_id'),
                self.payload.get('target_version_at_submission'),
                self.user_id, self.payload.get('change'),
                self.payload.get('description'))
        except utils.ValidationError as e:
            raise self.InvalidInputException(e)

        # TODO(#10513) : Find a way to save the images before the suggestion is
        # created.
        suggestion_image_context = suggestion.image_context

        new_image_filenames = (
            suggestion.get_new_image_filenames_added_in_suggestion())
        for filename in new_image_filenames:
            image = self.request.get(filename)
            if not image:
                logging.exception(
                    'Image not provided for file with name %s when the '
                    ' suggestion with target id %s was created.' % (
                        filename, suggestion.target_id))
                raise self.InvalidInputException(
                    'No image data provided for file with name %s.'
                    % (filename))
            try:
                file_format = (
                    image_validation_services.validate_image_and_filename(
                        image, filename))
            except utils.ValidationError as e:
                raise self.InvalidInputException('%s' % (e))
            image_is_compressible = (
                file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS)
            fs_services.save_original_and_compressed_versions_of_image(
                filename, suggestion_image_context, suggestion.target_id,
                image, 'image', image_is_compressible)

        target_entity_html_list = suggestion.get_target_entity_html_strings()
        target_image_filenames = (
            html_cleaner.get_image_filenames_from_html_strings(
                target_entity_html_list))

        fs_services.copy_images(
            suggestion.target_type, suggestion.target_id,
            suggestion_image_context, suggestion.target_id,
            target_image_filenames)

        self.render_json(self.values)
    def get_new_image_filenames_added_in_suggestion(self):
        """Returns the list of newly added image filenames in the suggestion.

        Returns:
            list(str). A list of newly added image filenames in the suggestion.
        """
        html_list = self.get_all_html_content_strings()
        all_image_filenames = (
            html_cleaner.get_image_filenames_from_html_strings(html_list))

        target_entity_html_list = self.get_target_entity_html_strings()
        target_image_filenames = (
            html_cleaner.get_image_filenames_from_html_strings(
                target_entity_html_list))

        new_image_filenames = utils.compute_list_difference(
            all_image_filenames, target_image_filenames)

        return new_image_filenames
예제 #9
0
def get_image_filenames_from_skill(skill):
    """Get the image filenames from the skill.

    Args:
        skill: Skill. The skill itself.

    Returns:
        list(str). List containing the name of the image files in skill.
    """
    html_list = skill.get_all_html_content_strings()
    return html_cleaner.get_image_filenames_from_html_strings(html_list)
예제 #10
0
파일: suggestion.py 프로젝트: oppia/oppia
    def put(self, target_id, suggestion_id):
        """Handles PUT requests.

        Args:
            target_id: str. The ID of the suggestion target.
            suggestion_id: str. The ID of the suggestion.
        """
        if suggestion_id.split('.')[0] != feconf.ENTITY_TYPE_SKILL:
            raise self.InvalidInputException(
                'This handler allows actions only on suggestions to skills.')

        if suggestion_id.split('.')[1] != target_id:
            raise self.InvalidInputException(
                'The skill id provided does not match the skill id present as '
                'part of the suggestion_id')

        action = self.payload.get('action')

        if action == constants.ACTION_ACCEPT_SUGGESTION:
            # Question suggestions do not use commit messages.
            suggestion_services.accept_suggestion(
                suggestion_id, self.user_id, 'UNUSED_COMMIT_MESSAGE',
                self.payload.get('review_message'))

            suggestion = suggestion_services.get_suggestion_by_id(
                suggestion_id)
            target_entity_html_list = (
                suggestion.get_target_entity_html_strings())
            target_image_filenames = (
                html_cleaner.get_image_filenames_from_html_strings(
                    target_entity_html_list))

            fs_services.copy_images(suggestion.target_type,
                                    suggestion.target_id,
                                    feconf.IMAGE_CONTEXT_QUESTION_SUGGESTIONS,
                                    suggestion.target_id,
                                    target_image_filenames)
        elif action == constants.ACTION_REJECT_SUGGESTION:
            suggestion_services.reject_suggestion(
                suggestion_id, self.user_id,
                self.payload.get('review_message'))
        else:
            raise self.InvalidInputException('Invalid action.')

        self.render_json(self.values)
예제 #11
0
def _upload_suggestion_images(request, suggestion, filenames):
    """Saves a suggestion's images to storage.

    Args:
        request: webapp2.Request. Request object containing a mapping of image
            filename to image blob.
        suggestion: BaseSuggestion. The suggestion for which images are being
            uploaded.
        filenames: list(str). The image filenames.
    """
    suggestion_image_context = suggestion.image_context
    # TODO(#10513) : Find a way to save the images before the suggestion is
    # created.
    for filename in filenames:
        image = request.get(filename)
        if not image:
            logging.exception(
                'Image not provided for file with name %s when the '
                ' suggestion with target id %s was created.' %
                (filename, suggestion.target_id))
            raise base.BaseHandler.InvalidInputException(
                'No image data provided for file with name %s.' % (filename))
        try:
            file_format = (
                image_validation_services.validate_image_and_filename(
                    image, filename))
        except utils.ValidationError as e:
            raise base.BaseHandler.InvalidInputException('%s' % (e))
        image_is_compressible = (file_format
                                 in feconf.COMPRESSIBLE_IMAGE_FORMATS)
        fs_services.save_original_and_compressed_versions_of_image(
            filename, suggestion_image_context, suggestion.target_id, image,
            'image', image_is_compressible)

    target_entity_html_list = suggestion.get_target_entity_html_strings()
    target_image_filenames = (
        html_cleaner.get_image_filenames_from_html_strings(
            target_entity_html_list))

    fs_services.copy_images(suggestion.target_type, suggestion.target_id,
                            suggestion_image_context, suggestion.target_id,
                            target_image_filenames)
예제 #12
0
    def post(self):
        """Handles POST requests."""
        skill_ids = self.payload.get('skill_ids')

        if not skill_ids:
            raise self.InvalidInputException(
                'skill_ids parameter isn\'t present in the payload')

        if len(skill_ids) > constants.MAX_SKILLS_PER_QUESTION:
            raise self.InvalidInputException(
                'More than %d QuestionSkillLinks for one question '
                'is not supported.' % constants.MAX_SKILLS_PER_QUESTION)
        try:
            for skill_id in skill_ids:
                skill_domain.Skill.require_valid_skill_id(skill_id)
        except Exception as e:
            raise self.InvalidInputException('Skill ID(s) aren\'t valid: ', e)

        try:
            skill_fetchers.get_multi_skills(skill_ids)
        except Exception as e:
            raise self.PageNotFoundException(e)

        question_dict = self.payload.get('question_dict')
        if ((question_dict['id'] is not None)
                or ('question_state_data' not in question_dict)
                or ('language_code' not in question_dict)
                or (question_dict['version'] != 0)):
            raise self.InvalidInputException(
                'Question Data should contain id, state data, language code, '
                + 'and its version should be set as 0')

        question_dict['question_state_data_schema_version'] = (
            feconf.CURRENT_STATE_SCHEMA_VERSION)
        question_dict['id'] = question_services.get_new_question_id()
        question_dict['linked_skill_ids'] = skill_ids

        try:
            question = question_domain.Question.from_dict(question_dict)
        except Exception as e:
            raise self.InvalidInputException('Question structure is invalid:',
                                             e)

        skill_difficulties = self.payload.get('skill_difficulties')

        if not skill_difficulties:
            raise self.InvalidInputException(
                'skill_difficulties not present in the payload')
        if len(skill_ids) != len(skill_difficulties):
            raise self.InvalidInputException(
                'Skill difficulties don\'t match up with skill IDs')

        try:
            skill_difficulties = [
                float(difficulty) for difficulty in skill_difficulties
            ]
        except (ValueError, TypeError) as e:
            raise self.InvalidInputException(
                'Skill difficulties must be a float value') from e
        if any((difficulty < 0 or difficulty > 1)
               for difficulty in skill_difficulties):
            raise self.InvalidInputException(
                'Skill difficulties must be between 0 and 1')

        question_services.add_question(self.user_id, question)
        question_services.link_multiple_skills_for_question(
            self.user_id, question.id, skill_ids, skill_difficulties)
        html_list = question.question_state_data.get_all_html_content_strings()
        filenames = (
            html_cleaner.get_image_filenames_from_html_strings(html_list))
        image_validation_error_message_suffix = (
            'Please go to the question editor for question with id %s and edit '
            'the image.' % question.id)
        for filename in filenames:
            image = self.request.get(filename)
            if not image:
                logging.exception(
                    'Image not provided for file with name %s when the question'
                    ' with id %s was created.' % (filename, question.id))
                raise self.InvalidInputException(
                    'No image data provided for file with name %s. %s' %
                    (filename, image_validation_error_message_suffix))
            try:
                file_format = (
                    image_validation_services.validate_image_and_filename(
                        image, filename))
            except utils.ValidationError as e:
                e = '%s %s' % (e, image_validation_error_message_suffix)
                raise self.InvalidInputException(e)
            image_is_compressible = (file_format
                                     in feconf.COMPRESSIBLE_IMAGE_FORMATS)
            fs_services.save_original_and_compressed_versions_of_image(
                filename, feconf.ENTITY_TYPE_QUESTION, question.id, image,
                'image', image_is_compressible)

        self.values.update({'question_id': question.id})
        self.render_json(self.values)