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)
def test_get_image_filenames_from_html_strings(self): html_strings = [ '<oppia-noninteractive-image ' 'filepath-with-value=""img.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image><oppia-noninteractive-image ' 'filepath-with-value=""img2.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image>', '<oppia-noninteractive-image ' 'filepath-with-value=""img3.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image><oppia-noninteractive-image ' 'filepath-with-value=""img4.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image>', '<oppia-noninteractive-image ' 'filepath-with-value=""img5.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image>' '<oppia-noninteractive-math math_content-with-value="{&quo' 't;raw_latex&quot;:&quot;+,-,-,+&quot;,&quot;sv' 'g_filename&quot;:&quot;math1.svg&quot;}"></oppia-n' 'oninteractive-math>' '<oppia-noninteractive-math math_content-with-value="{&quo' 't;raw_latex&quot;:&quot;x^2&quot;,&quot;sv' 'g_filename&quot;:&quot;math2.svg&quot;}"></oppia-n' 'oninteractive-math>' '<oppia-noninteractive-math math_content-with-value="{&quo' 't;raw_latex&quot;:&quot;(x-1)(x-2)^2&quot;,&quot' ';svg_filename&quot;:&quot;math3.svg&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))
def test_get_image_filenames_from_html_strings(self): html_strings = [ '<oppia-noninteractive-image ' 'filepath-with-value=""img.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image><oppia-noninteractive-image ' 'filepath-with-value=""img2.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image>', '<oppia-noninteractive-image ' 'filepath-with-value=""img3.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image><oppia-noninteractive-image ' 'filepath-with-value=""img4.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</oppia-noninteractive-image>', '<oppia-noninteractive-svgdiagram ' 'svg_filename-with-value=""img5.svg""' ' alt-with-value=""Image"">' '</oppia-noninteractive-svgdiagram><oppia-noninteractive-svgdiag' 'ram svg_filename-with-value=""img6.svg""' ' alt-with-value=""Image"">' '</oppia-noninteractive-svgdiagram><oppia-noninteractive-image ' 'filepath-with-value=""img7.svg"" caption-with-value=' '"""" alt-with-value=""Image"">' '</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))
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))
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())
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)
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
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)
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)
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)
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)