def post(self): description = self.payload.get('description') linked_topic_ids = self.payload.get('linked_topic_ids') explanation_dict = self.payload.get('explanation_dict') rubrics = self.payload.get('rubrics') if not isinstance(rubrics, list): raise self.InvalidInputException('Rubrics should be a list.') if not isinstance(explanation_dict, dict): raise self.InvalidInputException('Explanation should be a dict.') try: state_domain.SubtitledHtml.from_dict(explanation_dict) except: raise self.InvalidInputException( 'Explanation should be a valid SubtitledHtml dict.') rubrics = [skill_domain.Rubric.from_dict(rubric) for rubric in rubrics] new_skill_id = skill_services.get_new_skill_id() if linked_topic_ids is not None: topics = topic_fetchers.get_topics_by_ids(linked_topic_ids) for topic in topics: if topic is None: raise self.InvalidInputException topic_services.add_uncategorized_skill(self.user_id, topic.id, new_skill_id) skill_domain.Skill.require_valid_description(description) skill = skill_domain.Skill.create_default_skill( new_skill_id, description, rubrics) skill.update_explanation(explanation_dict) skill_services.save_new_skill(self.user_id, skill) self.render_json({'skillId': new_skill_id})
def post(self): if not feconf.ENABLE_NEW_STRUCTURES: raise self.PageNotFoundException topic_id = self.payload.get('topic_id') if topic_id is not None: topic = topic_services.get_topic_by_id(topic_id, strict=False) if topic is None: raise self.InvalidInputException description = self.payload.get('description') skill_domain.Skill.require_valid_description(description) new_skill_id = skill_services.get_new_skill_id() skill = skill_domain.Skill.create_default_skill( new_skill_id, description) skill_services.save_new_skill(self.user_id, skill) if topic_id is not None: topic_services.add_uncategorized_skill( self.user_id, topic_id, new_skill_id) self.render_json({ 'skillId': new_skill_id })
def test_add_uncategorized_skill(self): topic_services.add_uncategorized_skill(self.user_id_admin, self.TOPIC_ID, 'skill_id_3') topic = topic_services.get_topic_by_id(self.TOPIC_ID) self.assertEqual(topic.uncategorized_skill_ids, [self.skill_id_1, self.skill_id_2, 'skill_id_3']) topic_commit_log_entry = ( topic_models.TopicCommitLogEntryModel.get_commit(self.TOPIC_ID, 3)) self.assertEqual(topic_commit_log_entry.commit_type, 'edit') self.assertEqual(topic_commit_log_entry.topic_id, self.TOPIC_ID) self.assertEqual(topic_commit_log_entry.user_id, self.user_id_admin) self.assertEqual(topic_commit_log_entry.commit_message, 'Added skill_id_3 to uncategorized skill ids')
def post(self): description = self.payload.get('description') linked_topic_ids = self.payload.get('linked_topic_ids') new_skill_id = skill_services.get_new_skill_id() if linked_topic_ids is not None: topics = topic_fetchers.get_topics_by_ids(linked_topic_ids) for topic in topics: if topic is None: raise self.InvalidInputException topic_services.add_uncategorized_skill(self.user_id, topic.id, new_skill_id) skill_domain.Skill.require_valid_description(description) skill = skill_domain.Skill.create_default_skill( new_skill_id, description) skill_services.save_new_skill(self.user_id, skill) self.render_json({'skillId': new_skill_id})
def post(self): description = self.normalized_payload.get('description') linked_topic_ids = self.normalized_payload.get('linked_topic_ids') explanation_dict = self.normalized_payload.get('explanation_dict') rubrics = self.normalized_payload.get('rubrics') files = self.normalized_payload.get('files') new_skill_id = skill_services.get_new_skill_id() if linked_topic_ids is not None: topics = topic_fetchers.get_topics_by_ids(linked_topic_ids) for topic in topics: if topic is None: raise self.InvalidInputException topic_services.add_uncategorized_skill(self.user_id, topic.id, new_skill_id) if skill_services.does_skill_with_description_exist(description): raise self.InvalidInputException( 'Skill description should not be a duplicate.') skill = skill_domain.Skill.create_default_skill( new_skill_id, description, rubrics) skill.update_explanation(explanation_dict) image_filenames = skill_services.get_image_filenames_from_skill(skill) skill_services.save_new_skill(self.user_id, skill) for filename in image_filenames: base64_image = files.get(filename) bytes_image = base64.decodebytes(base64_image.encode('utf-8')) file_format = ( image_validation_services.validate_image_and_filename( bytes_image, filename)) image_is_compressible = (file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS) fs_services.save_original_and_compressed_versions_of_image( filename, feconf.ENTITY_TYPE_SKILL, skill.id, bytes_image, 'image', image_is_compressible) self.render_json({'skillId': new_skill_id})
def _publish_valid_topic(self, topic, uncategorized_skill_ids): """Saves and publishes a valid topic with linked skills and subtopic. Args: topic: Topic. The topic to be saved and published. uncategorized_skill_ids: list(str). List of uncategorized skills IDs to add to the supplied topic. """ topic.thumbnail_filename = 'thumbnail.svg' topic.thumbnail_bg_color = '#C6DCDA' subtopic_id = 1 subtopic_skill_id = 'subtopic_skill_id' + topic.id topic.subtopics = [ topic_domain.Subtopic( subtopic_id, 'Title', [subtopic_skill_id], 'image.svg', constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0], 21131, 'dummy-subtopic') ] topic.next_subtopic_id = 2 subtopic_page = ( subtopic_page_domain.SubtopicPage.create_default_subtopic_page( subtopic_id, topic.id)) subtopic_page_services.save_subtopic_page( self.owner_id, subtopic_page, 'Added subtopic', [ topic_domain.TopicChange({ 'cmd': topic_domain.CMD_ADD_SUBTOPIC, 'subtopic_id': 1, 'title': 'Sample' }) ]) topic_services.save_new_topic(self.owner_id, topic) topic_services.publish_topic(topic.id, self.admin_id) for skill_id in uncategorized_skill_ids: self.save_new_skill(skill_id, self.admin_id, description='skill_description') topic_services.add_uncategorized_skill(self.admin_id, topic.id, skill_id)
def post(self): description = self.payload.get('description') linked_topic_ids = self.payload.get('linked_topic_ids') explanation_dict = self.payload.get('explanation_dict') rubrics = self.payload.get('rubrics') if not isinstance(rubrics, list): raise self.InvalidInputException('Rubrics should be a list.') if not isinstance(explanation_dict, dict): raise self.InvalidInputException('Explanation should be a dict.') try: subtitled_html = ( state_domain.SubtitledHtml.from_dict(explanation_dict)) subtitled_html.validate() except Exception as e: raise self.InvalidInputException( 'Explanation should be a valid SubtitledHtml dict.') from e rubrics = [skill_domain.Rubric.from_dict(rubric) for rubric in rubrics] new_skill_id = skill_services.get_new_skill_id() if linked_topic_ids is not None: topics = topic_fetchers.get_topics_by_ids(linked_topic_ids) for topic in topics: if topic is None: raise self.InvalidInputException topic_services.add_uncategorized_skill(self.user_id, topic.id, new_skill_id) skill_domain.Skill.require_valid_description(description) if skill_services.does_skill_with_description_exist(description): raise self.InvalidInputException( 'Skill description should not be a duplicate.') skill = skill_domain.Skill.create_default_skill( new_skill_id, description, rubrics) skill.update_explanation( state_domain.SubtitledHtml.from_dict(explanation_dict)) image_filenames = skill_services.get_image_filenames_from_skill(skill) skill_services.save_new_skill(self.user_id, skill) image_validation_error_message_suffix = ( 'Please go to oppia.org/skill_editor/%s to edit ' 'the image.' % skill.id) for filename in image_filenames: image = self.request.get(filename) if not image: logging.exception( 'Image not provided for file with name %s when the skill ' 'with id %s was created.' % (filename, skill.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_SKILL, skill.id, image, 'image', image_is_compressible) self.render_json({'skillId': new_skill_id})
def setUp(self): super(ContributionOpportunitiesHandlerTest, self).setUp() self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME) self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME) self.admin_id = self.get_user_id_from_email(self.ADMIN_EMAIL) self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL) self.set_admins([self.ADMIN_USERNAME]) explorations = [exp_domain.Exploration.create_default_exploration( '%s' % i, title='title %d' % i, category='category%d' % i, ) for i in python_utils.RANGE(2)] for exp in explorations: exp_services.save_new_exploration(self.owner_id, exp) topic = topic_domain.Topic.create_default_topic( topic_id='0', name='topic', abbreviated_name='abbrev') topic_services.save_new_topic(self.owner_id, topic) self.skill_id_0 = 'skill_id_0' self.skill_id_1 = 'skill_id_1' self.skill_ids = [self.skill_id_0, self.skill_id_1] for skill_id in self.skill_ids: self.save_new_skill(skill_id, self.admin_id, 'skill_description') topic_services.add_uncategorized_skill( self.admin_id, '0', skill_id) self.expected_skill_opportunity_dict_0 = { 'id': self.skill_id_0, 'skill_description': 'skill_description', 'question_count': 0, 'topic_name': 'topic' } self.expected_skill_opportunity_dict_1 = { 'id': self.skill_id_1, 'skill_description': 'skill_description', 'question_count': 0, 'topic_name': 'topic' } stories = [story_domain.Story.create_default_story( '%s' % i, title='title %d' % i, corresponding_topic_id='0' ) for i in python_utils.RANGE(2)] for index, story in enumerate(stories): story.language_code = 'en' story_services.save_new_story(self.owner_id, story) topic_services.add_canonical_story( self.owner_id, topic.id, story.id) story_services.update_story( self.owner_id, story.id, [story_domain.StoryChange({ 'cmd': 'add_story_node', 'node_id': 'node_1', 'title': 'Node1', }), story_domain.StoryChange({ 'cmd': 'update_story_node_property', 'property_name': 'exploration_id', 'node_id': 'node_1', 'old_value': None, 'new_value': explorations[index].id })], 'Changes.') self.expected_opportunity_dict_1 = { 'id': '0', 'topic_name': 'topic', 'story_title': 'title 0', 'chapter_title': 'Node1', 'content_count': 2, 'translation_counts': {} } self.expected_opportunity_dict_2 = { 'id': '1', 'topic_name': 'topic', 'story_title': 'title 1', 'chapter_title': 'Node1', 'content_count': 2, 'translation_counts': {} }
def setUp(self): super(ContributionOpportunitiesHandlerTest, self).setUp() self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME) self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME) self.admin_id = self.get_user_id_from_email(self.ADMIN_EMAIL) self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL) self.set_admins([self.ADMIN_USERNAME]) explorations = [ self.save_new_valid_exploration('%s' % i, self.owner_id, title='title %d' % i, category='category%d' % i, end_state_name='End State') for i in python_utils.RANGE(2) ] for exp in explorations: self.publish_exploration(self.owner_id, exp.id) topic = topic_domain.Topic.create_default_topic( '0', 'topic', 'abbrev', 'description') topic.thumbnail_filename = 'thumbnail.svg' topic.thumbnail_bg_color = '#C6DCDA' topic_services.save_new_topic(self.owner_id, topic) topic_services.publish_topic('0', self.admin_id) self.skill_id_0 = 'skill_id_0' self.skill_id_1 = 'skill_id_1' self.skill_ids = [self.skill_id_0, self.skill_id_1] for skill_id in self.skill_ids: self.save_new_skill(skill_id, self.admin_id, description='skill_description') topic_services.add_uncategorized_skill(self.admin_id, '0', skill_id) self.expected_skill_opportunity_dict_0 = { 'id': self.skill_id_0, 'skill_description': 'skill_description', 'question_count': 0, 'topic_name': 'topic' } self.expected_skill_opportunity_dict_1 = { 'id': self.skill_id_1, 'skill_description': 'skill_description', 'question_count': 0, 'topic_name': 'topic' } stories = [ story_domain.Story.create_default_story('%s' % i, 'title %d' % i, 'description %d' % i, '0', 'title-%s' % chr(97 + i)) for i in python_utils.RANGE(2) ] for index, story in enumerate(stories): story.language_code = 'en' story_services.save_new_story(self.owner_id, story) topic_services.add_canonical_story(self.owner_id, topic.id, story.id) topic_services.publish_story(topic.id, story.id, self.admin_id) story_services.update_story(self.owner_id, story.id, [ story_domain.StoryChange({ 'cmd': 'add_story_node', 'node_id': 'node_1', 'title': 'Node1', }), story_domain.StoryChange({ 'cmd': 'update_story_node_property', 'property_name': 'exploration_id', 'node_id': 'node_1', 'old_value': None, 'new_value': explorations[index].id }) ], 'Changes.') # The content_count is 4 for the expected dicts below since a valid # exploration with EndExploration is created above, so the content in # the last state is also included in the count. This content includes: # 2 content, 1 TextInput interaction customization argument # (placeholder), and 1 outcome. self.expected_opportunity_dict_1 = { 'id': '0', 'topic_name': 'topic', 'story_title': 'title 0', 'chapter_title': 'Node1', 'content_count': 2, 'translation_counts': {} } self.expected_opportunity_dict_2 = { 'id': '1', 'topic_name': 'topic', 'story_title': 'title 1', 'chapter_title': 'Node1', 'content_count': 2, 'translation_counts': {} }
def setUp(self): super(ContributionOpportunitiesHandlerTest, self).setUp() self.signup(self.CURRICULUM_ADMIN_EMAIL, self.CURRICULUM_ADMIN_USERNAME) self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME) self.admin_id = self.get_user_id_from_email(self.CURRICULUM_ADMIN_EMAIL) self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL) self.set_curriculum_admins([self.CURRICULUM_ADMIN_USERNAME]) explorations = [self.save_new_valid_exploration( '%s' % i, self.owner_id, title='title %d' % i, category='category%d' % i, end_state_name='End State', correctness_feedback_enabled=True ) for i in python_utils.RANGE(2)] for exp in explorations: self.publish_exploration(self.owner_id, exp.id) topic_id = '0' topic = topic_domain.Topic.create_default_topic( topic_id, 'topic', 'abbrev', 'description') topic.thumbnail_filename = 'thumbnail.svg' topic.thumbnail_bg_color = '#C6DCDA' topic.subtopics = [ topic_domain.Subtopic( 1, 'Title', ['skill_id_3'], 'image.svg', constants.ALLOWED_THUMBNAIL_BG_COLORS['subtopic'][0], 21131, 'dummy-subtopic-three')] topic.next_subtopic_id = 2 topic_services.save_new_topic(self.owner_id, topic) topic_services.publish_topic(topic_id, self.admin_id) self.skill_id_0 = 'skill_id_0' self.skill_id_1 = 'skill_id_1' self.skill_ids = [self.skill_id_0, self.skill_id_1] for skill_id in self.skill_ids: self.save_new_skill( skill_id, self.admin_id, description='skill_description') topic_services.add_uncategorized_skill( self.admin_id, topic_id, skill_id) # Add skill opportunity topic to a classroom. config_services.set_property( self.admin_id, 'classroom_pages_data', [{ 'name': 'math', 'url_fragment': 'math-one', 'topic_ids': [topic_id], 'course_details': '', 'topic_list_intro': '' }]) self.expected_skill_opportunity_dict_0 = { 'id': self.skill_id_0, 'skill_description': 'skill_description', 'question_count': 0, 'topic_name': 'topic' } self.expected_skill_opportunity_dict_1 = { 'id': self.skill_id_1, 'skill_description': 'skill_description', 'question_count': 0, 'topic_name': 'topic' } stories = [story_domain.Story.create_default_story( '%s' % i, 'title %d' % i, 'description %d' % i, '0', 'title-%s' % chr(97 + i) ) for i in python_utils.RANGE(2)] for index, story in enumerate(stories): story.language_code = 'en' story_services.save_new_story(self.owner_id, story) topic_services.add_canonical_story( self.owner_id, topic.id, story.id) topic_services.publish_story( topic.id, story.id, self.admin_id) story_services.update_story( self.owner_id, story.id, [story_domain.StoryChange({ 'cmd': 'add_story_node', 'node_id': 'node_1', 'title': 'Node1', }), story_domain.StoryChange({ 'cmd': 'update_story_node_property', 'property_name': 'exploration_id', 'node_id': 'node_1', 'old_value': None, 'new_value': explorations[index].id })], 'Changes.') # The content_count is 4 for the expected dicts below since a valid # exploration with EndExploration is created above, so the content in # the last state is also included in the count. This content includes: # 2 content, 1 TextInput interaction customization argument # (placeholder), and 1 outcome. self.expected_opportunity_dict_1 = { 'id': '0', 'topic_name': 'topic', 'story_title': 'title 0', 'chapter_title': 'Node1', 'content_count': 2, 'translation_counts': {} } self.expected_opportunity_dict_2 = { 'id': '1', 'topic_name': 'topic', 'story_title': 'title 1', 'chapter_title': 'Node1', 'content_count': 2, 'translation_counts': {} } config_services.set_property( 'admin', 'contributor_dashboard_is_enabled', True)