def _record_node_completion(self, story_id, node_id, completed_node_ids, ordered_nodes): """Records node completion.""" if not constants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES: raise self.PageNotFoundException try: story_fetchers.get_node_index_by_story_id_and_node_id( story_id, node_id) except Exception as e: raise self.PageNotFoundException(e) next_exp_ids = [] next_node_id = None if node_id not in completed_node_ids: story_services.record_completed_node_in_story_context( self.user_id, story_id, node_id) completed_nodes = story_fetchers.get_completed_nodes_in_story( self.user_id, story_id) completed_node_ids = [ completed_node.id for completed_node in completed_nodes ] for node in ordered_nodes: if node.id not in completed_node_ids: next_exp_ids = [node.exploration_id] next_node_id = node.id break return (next_exp_ids, next_node_id, completed_node_ids)
def test_get_multi_users_progress_in_stories(self) -> None: all_users_stories_progress = ( story_fetchers.get_multi_users_progress_in_stories( [self.USER_ID], [self.story_id, 'invalid_story_id'])) all_stories = story_fetchers.get_stories_by_ids( [self.story_id, 'invalid_story_id']) # Should return None for invalid story ID. self.assertIsNone(all_stories[1]) user_stories_progress = all_users_stories_progress[self.USER_ID] self.assertEqual(len(user_stories_progress), 1) assert all_stories[0] is not None self.assertEqual(user_stories_progress[0]['id'], self.story_id) self.assertEqual(user_stories_progress[0]['completed_node_titles'], []) self.assertEqual(len(user_stories_progress[0]['all_node_dicts']), len(all_stories[0].story_contents.nodes)) self.assertEqual(user_stories_progress[0]['topic_name'], 'Topic') story_services.record_completed_node_in_story_context( # type: ignore[no-untyped-call] self.USER_ID, self.story_id, self.NODE_ID_1) all_users_stories_progress = ( story_fetchers.get_multi_users_progress_in_stories( [self.USER_ID], [self.story_id, 'invalid_story_id'])) user_stories_progress = all_users_stories_progress[self.USER_ID] self.assertEqual(len(user_stories_progress), 1) # Ruling out the possibility of None for mypy type checking. assert user_stories_progress[0] is not None self.assertEqual(user_stories_progress[0]['id'], self.story_id) self.assertEqual(user_stories_progress[0]['completed_node_titles'], ['Title 1']) self.assertEqual(user_stories_progress[0]['topic_name'], 'Topic')
def test_get_fails_when_acquired_skills_dont_exist(self): story_id = 'story_id_3' node_id = 'node_1' node = { 'id': node_id, 'title': 'Title 1', 'destination_node_ids': [], 'acquired_skill_ids': ['skill_id_3'], 'prerequisite_skill_ids': [], 'outline': '', 'outline_is_finalized': False, 'exploration_id': self.exp_id } story = story_domain.Story.create_default_story( story_id, 'Public Story Title', self.topic_id) story.story_contents.nodes = [ story_domain.StoryNode.from_dict(node) ] story.story_contents.initial_node_id = node_id story.story_contents.next_node_id = self.node_id_2 story_services.save_new_story(self.admin_id, story) story_services.publish_story(story_id, self.admin_id) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True): story_services.record_completed_node_in_story_context( self.viewer_id, story_id, node_id) self.get_json( '%s/%s' % ( feconf.REVIEW_TEST_DATA_URL_PREFIX, story_id), expected_status_int=404)
def test_mark_story_and_topic_as_completed_and_learnt(self): csrf_token = self.get_new_csrf_token() learner_progress_services.validate_and_add_topic_to_learn_goal( self.viewer_id, self.TOPIC_ID) self.assertEqual( len( learner_goals_services.get_all_topic_ids_to_learn( self.viewer_id)), 1) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_1) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_VIEWER_UPDATES', True): self.post_json('%s/staging/topic/%s/%s' % (feconf.STORY_PROGRESS_URL_PREFIX, self.STORY_URL_FRAGMENT, self.NODE_ID_3), {}, csrf_token=csrf_token) self.assertEqual( len( learner_progress_services.get_all_learnt_topic_ids( self.viewer_id)), 1) self.assertEqual( len( learner_goals_services.get_all_topic_ids_to_learn( self.viewer_id)), 0) self.assertEqual( len( learner_progress_services.get_all_completed_story_ids( self.viewer_id)), 1)
def test_get_latest_completed_node_ids(self) -> None: self.assertEqual( story_fetchers.get_latest_completed_node_ids( self.USER_ID, self.story_id), []) story_services.record_completed_node_in_story_context( # type: ignore[no-untyped-call] self.USER_ID, self.story_id, self.NODE_ID_1) self.assertEqual( story_fetchers.get_latest_completed_node_ids( self.USER_ID, self.story_id), [self.NODE_ID_1])
def test_get_latest_completed_node_ids(self): self.assertEqual( story_fetchers.get_latest_completed_node_ids( self.USER_ID, self.STORY_ID), []) story_services.record_completed_node_in_story_context( self.USER_ID, self.STORY_ID, self.NODE_ID_1) self.assertEqual( story_fetchers.get_latest_completed_node_ids( self.USER_ID, self.STORY_ID), [self.NODE_ID_1])
def test_any_user_can_access_review_tests_data(self): with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True): story_services.record_completed_node_in_story_context( self.viewer_id, self.story_id_1, self.node_id) json_response = self.get_json( '%s/%s' % (feconf.REVIEW_TEST_DATA_URL_PREFIX, self.story_id_1)) self.assertEqual(len(json_response['skill_ids']), 2) self.assertEqual(json_response['skill_ids'][0], 'skill_id_1') self.assertEqual(json_response['skill_ids'][1], 'skill_id_2')
def test_get_completed_node_id(self): self.assertEqual( story_fetchers.get_completed_node_ids('randomID', 'someID'), []) story_services.record_completed_node_in_story_context( self.USER_ID, self.STORY_ID, self.NODE_ID_1) story_services.record_completed_node_in_story_context( self.USER_ID, self.STORY_ID, self.NODE_ID_2) self.assertEqual( story_fetchers.get_completed_node_ids(self.USER_ID, self.STORY_ID), [self.NODE_ID_1, self.NODE_ID_2])
def test_any_user_can_access_review_tests_data(self): story_services.record_completed_node_in_story_context( self.viewer_id, self.story_id_1, self.node_id) json_response = self.get_json( '%s/staging/topic/%s' % (feconf.REVIEW_TEST_DATA_URL_PREFIX, self.story_url_fragment_1)) self.assertEqual(len(json_response['skill_descriptions']), 2) self.assertEqual(json_response['skill_descriptions']['skill_id_1'], 'Skill 1') self.assertEqual(json_response['skill_descriptions']['skill_id_2'], 'Skill 2')
def test_get_completed_nodes_in_story(self): story = story_fetchers.get_story_by_id(self.STORY_ID) story_services.record_completed_node_in_story_context( self.USER_ID, self.STORY_ID, self.NODE_ID_1) story_services.record_completed_node_in_story_context( self.USER_ID, self.STORY_ID, self.NODE_ID_2) for ind, completed_node in enumerate( story_fetchers.get_completed_nodes_in_story( self.USER_ID, self.STORY_ID)): self.assertEqual(completed_node.to_dict(), story.story_contents.nodes[ind].to_dict())
def test_get_completed_nodes_in_story(self) -> None: story = story_fetchers.get_story_by_id(self.story_id) story_services.record_completed_node_in_story_context( # type: ignore[no-untyped-call] self.USER_ID, self.story_id, self.NODE_ID_1) story_services.record_completed_node_in_story_context( # type: ignore[no-untyped-call] self.USER_ID, self.story_id, self.NODE_ID_2) for ind, completed_node in enumerate( story_fetchers.get_completed_nodes_in_story( self.USER_ID, self.story_id)): self.assertEqual(completed_node.to_dict(), story.story_contents.nodes[ind].to_dict())
def post(self, story_id, node_id): if not constants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES: raise self.PageNotFoundException try: story_fetchers.get_node_index_by_story_id_and_node_id( story_id, node_id) except Exception as e: raise self.PageNotFoundException(e) story = story_fetchers.get_story_by_id(story_id) completed_nodes = story_fetchers.get_completed_nodes_in_story( self.user_id, story_id) completed_node_ids = [ completed_node.id for completed_node in completed_nodes ] ordered_nodes = [ node for node in story.story_contents.get_ordered_nodes() ] next_exp_ids = [] next_node_id = None if not node_id in completed_node_ids: story_services.record_completed_node_in_story_context( self.user_id, story_id, node_id) completed_nodes = story_fetchers.get_completed_nodes_in_story( self.user_id, story_id) completed_node_ids = [ completed_node.id for completed_node in completed_nodes ] for node in ordered_nodes: if node.id not in completed_node_ids: next_exp_ids = [node.exploration_id] next_node_id = node.id break ready_for_review_test = False exp_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( next_exp_ids)) if ((len(exp_summaries) != 0 and len(completed_nodes) % constants.NUM_EXPLORATIONS_PER_REVIEW_TEST == 0) or (len(completed_nodes) == len(ordered_nodes))): ready_for_review_test = True return self.render_json({ 'summaries': exp_summaries, 'ready_for_review_test': ready_for_review_test, 'next_node_id': next_node_id })
def post(self, story_id, node_id): if not constants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES: raise self.PageNotFoundException try: story_fetchers.get_node_index_by_story_id_and_node_id( story_id, node_id) except Exception as e: raise self.PageNotFoundException(e) story_services.record_completed_node_in_story_context( self.user_id, story_id, node_id) return self.render_json({})
def test_post_returns_empty_list_when_user_completes_story(self): csrf_token = self.get_new_csrf_token() story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_1) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_VIEWER_UPDATES', True): json_response = self.post_json( '%s/%s/%s' % (feconf.STORY_PROGRESS_URL_PREFIX, self.STORY_ID, self.NODE_ID_3), {}, csrf_token=csrf_token) self.assertEqual(len(json_response['summaries']), 0) self.assertIsNone(json_response['next_node_id']) self.assertTrue(json_response['ready_for_review_test'])
def test_redirect_for_returning_user_with_completed_nodes(self): self.NEW_USER_EMAIL = '*****@*****.**' self.NEW_USER_USERNAME = '******' self.signup(self.NEW_USER_EMAIL, self.NEW_USER_USERNAME) self.login(self.NEW_USER_EMAIL) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_1) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_VIEWER_UPDATES', True): response = self.get_html_response( '%s/staging/topic/%s/%s' % ( feconf.STORY_PROGRESS_URL_PREFIX, self.STORY_URL_FRAGMENT, self.NODE_ID_1), expected_status_int=302) self.assertEqual( 'http://localhost/learn/staging/topic/story/title-one', response.headers['location'])
def test_get_fails_when_acquired_skills_dont_exist(self): node_id = 'node_1' node = { 'id': node_id, 'title': 'Title 1', 'description': 'Description 1', 'thumbnail_filename': 'image.svg', 'thumbnail_bg_color': constants.ALLOWED_THUMBNAIL_BG_COLORS['chapter'][0], 'thumbnail_size_in_bytes': 21131, 'destination_node_ids': [], 'acquired_skill_ids': ['skill_id_3'], 'prerequisite_skill_ids': [], 'outline': '', 'outline_is_finalized': False, 'exploration_id': self.exp_id } story = story_domain.Story.create_default_story( self.story_id_3, 'Public Story Title', 'Description', self.topic_id, 'public-story-title-two') story.story_contents.nodes = [story_domain.StoryNode.from_dict(node)] story.story_contents.initial_node_id = node_id story.story_contents.next_node_id = self.node_id_2 story_services.save_new_story(self.admin_id, story) topic_services.publish_story(self.topic_id, self.story_id_3, self.admin_id) story_services.record_completed_node_in_story_context( self.viewer_id, self.story_id_3, node_id) self.get_json( '%s/staging/topic/%s' % (feconf.REVIEW_TEST_DATA_URL_PREFIX, 'public-story-title-two'), expected_status_int=404)
def test_post_returns_ready_for_review_when_acquired_skills_exist(self): csrf_token = self.get_new_csrf_token() self.save_new_skill('skill_1', self.admin_id, description='Skill Description') self.save_new_question('question_1', self.admin_id, self._create_valid_question_data('ABC'), ['skill_1']) question_services.create_new_question_skill_link( self.admin_id, 'question_1', 'skill_1', 0.3) changelist = [ story_domain.StoryChange({ 'cmd': story_domain.CMD_UPDATE_STORY_NODE_PROPERTY, 'property_name': (story_domain.STORY_NODE_PROPERTY_ACQUIRED_SKILL_IDS), 'node_id': self.NODE_ID_1, 'old_value': [], 'new_value': ['skill_1'] }) ] story_services.update_story(self.admin_id, self.STORY_ID, changelist, 'Added acquired skill.') story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_1) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_VIEWER_UPDATES', True): json_response = self.post_json( '%s/staging/topic/%s/%s' % (feconf.STORY_PROGRESS_URL_PREFIX, self.STORY_URL_FRAGMENT, self.NODE_ID_3), {}, csrf_token=csrf_token) self.assertEqual(len(json_response['summaries']), 0) self.assertIsNone(json_response['next_node_id']) self.assertTrue(json_response['ready_for_review_test'])
class StoryNodeCompletionHandler(base.BaseHandler): """Marks a story node as completed after completing.""" GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON @acl_decorators.can_access_story_viewer_page def post(self, story_id, node_id): if not constants.ENABLE_NEW_STRUCTURE_PLAYERS: raise self.PageNotFoundException try: story_fetchers.get_node_index_by_story_id_and_node_id( story_id, node_id) except Exception, e: raise self.PageNotFoundException(e) story_services.record_completed_node_in_story_context( self.user_id, story_id, node_id) return self.render_json({})
def _record_completion(self, user_id, STORY_ID, node_id): story_services.record_completed_node_in_story_context( user_id, STORY_ID, node_id)
def _record_completion(self, user_id, STORY_ID, node_id): """Records the completion of a node in the context of a story.""" story_services.record_completed_node_in_story_context( user_id, STORY_ID, node_id)
def test_can_get_completed_chapters_count(self): self.save_new_topic(self.TOPIC_ID_1, self.owner_id, name=self.TOPIC_NAME_1, url_fragment='topic-one', description='A new topic', canonical_story_ids=[], additional_story_ids=[], uncategorized_skill_ids=[], subtopics=[self.subtopic_0], next_subtopic_id=1) self.save_new_story(self.STORY_ID_1, self.owner_id, self.TOPIC_ID_1) topic_services.add_canonical_story(self.owner_id, self.TOPIC_ID_1, self.STORY_ID_1) self.save_new_default_exploration(self.EXP_ID_1, self.owner_id, 'Title 1') self.publish_exploration(self.owner_id, self.EXP_ID_1) changelist = [ story_domain.StoryChange({ 'cmd': story_domain.CMD_ADD_STORY_NODE, 'node_id': 'node_1', 'title': 'Title 1' }), story_domain.StoryChange({ 'cmd': story_domain.CMD_UPDATE_STORY_NODE_PROPERTY, 'property_name': (story_domain.STORY_NODE_PROPERTY_EXPLORATION_ID), 'node_id': 'node_1', 'old_value': None, 'new_value': self.EXP_ID_1 }) ] story_services.update_story(self.owner_id, self.STORY_ID_1, changelist, 'Added first node.') topic_services.publish_story(self.TOPIC_ID_1, self.STORY_ID_1, self.admin_id) topic_services.publish_topic(self.TOPIC_ID_1, self.admin_id) self.login(self.CURRICULUM_ADMIN_EMAIL, is_super_admin=True) csrf_token = self.get_new_csrf_token() new_config_value = [{ 'name': 'math', 'url_fragment': 'math', 'topic_ids': [self.TOPIC_ID_1], 'course_details': '', 'topic_list_intro': '' }] payload = { 'action': 'save_config_properties', 'new_config_property_values': { config_domain.CLASSROOM_PAGES_DATA.name: (new_config_value), } } self.post_json('/adminhandler', payload, csrf_token=csrf_token) self.logout() self.login(self.VIEWER_EMAIL) self.assertEqual( self.get_json(feconf.LEARNER_COMPLETED_CHAPTERS_COUNT_DATA_URL) ['completed_chapters_count'], 0) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID_1, 'node_1') self.assertEqual( self.get_json(feconf.LEARNER_COMPLETED_CHAPTERS_COUNT_DATA_URL) ['completed_chapters_count'], 1) self.logout()
def test_record_completed_node_in_story_context(self): # Ensure that node completed within the context of a story are # recorded correctly. This test actually validates both # test_get_completed_node_ids and # test_get_next_node_id_to_be_completed_by_user. # By default, no completion model should exist for a given user and # story. completion_model = self._get_progress_model(self.owner_id, self.STORY_1_ID) self.assertIsNone(completion_model) # If the user 'completes an node', the model should record it. story_services.record_completed_node_in_story_context( self.owner_id, self.STORY_1_ID, self.NODE_ID_1) completion_model = self._get_progress_model(self.owner_id, self.STORY_1_ID) self.assertIsNotNone(completion_model) self.assertEqual(completion_model.completed_node_ids, [self.NODE_ID_1]) # If the same node is completed again within the context of this # story, it should not be duplicated. story_services.record_completed_node_in_story_context( self.owner_id, self.STORY_1_ID, self.NODE_ID_1) completion_model = self._get_progress_model(self.owner_id, self.STORY_1_ID) self.assertEqual(completion_model.completed_node_ids, [self.NODE_ID_1]) # If the same node and another are completed within the context # of a different story, it shouldn't affect this one. self.story = self.save_new_story(self.STORY_ID_1, self.USER_ID, 'Title', 'Description', 'Notes', self.TOPIC_ID) topic_services.add_canonical_story(self.USER_ID, self.TOPIC_ID, self.STORY_ID_1) story_services.record_completed_node_in_story_context( self.owner_id, self.STORY_ID_1, self.NODE_ID_1) story_services.record_completed_node_in_story_context( self.owner_id, self.STORY_ID_1, self.NODE_ID_2) completion_model = self._get_progress_model(self.owner_id, self.STORY_1_ID) self.assertEqual(completion_model.completed_node_ids, [self.NODE_ID_1]) # If two more nodes are completed, they are recorded. story_services.record_completed_node_in_story_context( self.owner_id, self.STORY_1_ID, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.owner_id, self.STORY_1_ID, self.NODE_ID_3) completion_model = self._get_progress_model(self.owner_id, self.STORY_1_ID) self.assertEqual(completion_model.completed_node_ids, [self.NODE_ID_1, self.NODE_ID_2, self.NODE_ID_3])
def setUp(self): super(LearnerGroupStudentProgressHandlerTests, self).setUp() self.signup(self.NEW_USER_EMAIL, self.NEW_USER_USERNAME) self.signup(self.CURRICULUM_ADMIN_EMAIL, self.CURRICULUM_ADMIN_USERNAME) self.signup(self.STUDENT_1_EMAIL, self.STUDENT_1_USERNAME) self.signup(self.STUDENT_2_EMAIL, self.STUDENT_2_USERNAME) self.STUDENT_ID_1 = self.get_user_id_from_email(self.STUDENT_1_EMAIL) self.STUDENT_ID_2 = self.get_user_id_from_email(self.STUDENT_2_EMAIL) self.facilitator_id = self.get_user_id_from_email(self.NEW_USER_EMAIL) self.admin_id = self.get_user_id_from_email( self.CURRICULUM_ADMIN_EMAIL) self.set_curriculum_admins([self.CURRICULUM_ADMIN_USERNAME]) self.admin = user_services.get_user_actions_info(self.admin_id) self.LEARNER_GROUP_ID = ( learner_group_fetchers.get_new_learner_group_id()) learner_group_services.create_learner_group( self.LEARNER_GROUP_ID, 'Learner Group Name', 'Description', [self.facilitator_id], [self.STUDENT_ID_1, self.STUDENT_ID_2], [self.SUBTOPIC_PAGE_ID_1], [self.STORY_ID_1]) # Set up topics, subtopics and stories for learner group syllabus. self.save_new_valid_exploration(self.EXP_ID_0, self.admin_id, title='Title 1', end_state_name='End', correctness_feedback_enabled=True) self.save_new_valid_exploration(self.EXP_ID_1, self.admin_id, title='Title 2', end_state_name='End', correctness_feedback_enabled=True) self.save_new_valid_exploration(self.EXP_ID_7, self.admin_id, title='Title 3', end_state_name='End', correctness_feedback_enabled=True) self.publish_exploration(self.admin_id, self.EXP_ID_0) self.publish_exploration(self.admin_id, self.EXP_ID_1) self.publish_exploration(self.admin_id, self.EXP_ID_7) story = story_domain.Story.create_default_story( self.STORY_ID_1, 'Title', 'Description', self.TOPIC_ID_1, self.STORY_URL_FRAGMENT) story.meta_tag_content = 'story meta content' exp_summary_dicts = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_0, self.EXP_ID_1, self.EXP_ID_7], user=self.admin)) self.node_1 = { 'id': self.NODE_ID_1, 'title': 'Title 1', 'description': 'Description 1', 'thumbnail_filename': 'image_1.svg', 'thumbnail_bg_color': constants.ALLOWED_THUMBNAIL_BG_COLORS['chapter'][0], 'thumbnail_size_in_bytes': 21131, 'destination_node_ids': ['node_3'], 'acquired_skill_ids': [], 'prerequisite_skill_ids': [], 'outline': '', 'outline_is_finalized': False, 'exploration_id': self.EXP_ID_1, 'exp_summary_dict': exp_summary_dicts[1], 'completed': False } self.node_2 = { 'id': self.NODE_ID_2, 'title': 'Title 2', 'description': 'Description 2', 'thumbnail_filename': 'image_2.svg', 'thumbnail_bg_color': constants.ALLOWED_THUMBNAIL_BG_COLORS['chapter'][0], 'thumbnail_size_in_bytes': 21131, 'destination_node_ids': ['node_1'], 'acquired_skill_ids': [], 'prerequisite_skill_ids': [], 'outline': '', 'outline_is_finalized': False, 'exploration_id': self.EXP_ID_0, 'exp_summary_dict': exp_summary_dicts[0], 'completed': True } self.node_3 = { 'id': self.NODE_ID_3, 'title': 'Title 3', 'description': 'Description 3', 'thumbnail_filename': 'image_3.svg', 'thumbnail_bg_color': constants.ALLOWED_THUMBNAIL_BG_COLORS['chapter'][0], 'thumbnail_size_in_bytes': 21131, 'destination_node_ids': [], 'acquired_skill_ids': [], 'prerequisite_skill_ids': [], 'outline': '', 'outline_is_finalized': False, 'exploration_id': self.EXP_ID_7, 'exp_summary_dict': exp_summary_dicts[2], 'completed': False } story.story_contents.nodes = [ story_domain.StoryNode.from_dict(self.node_1), story_domain.StoryNode.from_dict(self.node_2), story_domain.StoryNode.from_dict(self.node_3) ] self.nodes = story.story_contents.nodes story.story_contents.initial_node_id = 'node_2' story.story_contents.next_node_id = 'node_4' story_services.save_new_story(self.admin_id, story) self.subtopic_1 = topic_domain.Subtopic.create_default_subtopic( 1, 'Subtopic Title 1', 'sub-one-frag') self.subtopic_2 = topic_domain.Subtopic.create_default_subtopic( 2, 'Subtopic Title 2', 'sub-two-frag') self.SKILL_ID_1 = skill_services.get_new_skill_id() self.SKILL_ID_2 = skill_services.get_new_skill_id() self.subtopic_1.skill_ids = [self.SKILL_ID_1] self.subtopic_2.skill_ids = [self.SKILL_ID_2] self.save_new_topic(self.TOPIC_ID_1, 'user', name='Topic', description='A new topic', canonical_story_ids=[story.id], additional_story_ids=[], uncategorized_skill_ids=[], subtopics=[self.subtopic_1, self.subtopic_2], next_subtopic_id=3) topic_services.publish_topic(self.TOPIC_ID_1, self.admin_id) topic_services.publish_story(self.TOPIC_ID_1, self.STORY_ID_1, self.admin_id) # Add the invited students to the learner group. learner_group_services.add_student_to_learner_group( self.LEARNER_GROUP_ID, self.STUDENT_ID_1, True) learner_group_services.add_student_to_learner_group( self.LEARNER_GROUP_ID, self.STUDENT_ID_2, False) # Add some progress for the students. story_services.record_completed_node_in_story_context( self.STUDENT_ID_1, self.STORY_ID_1, self.NODE_ID_1) story_services.record_completed_node_in_story_context( self.STUDENT_ID_1, self.STORY_ID_1, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.STUDENT_ID_2, self.STORY_ID_1, self.NODE_ID_3) self.SKILL_IDS = [self.SKILL_ID_1, self.SKILL_ID_2] skill_services.create_user_skill_mastery(self.STUDENT_ID_1, self.SKILL_ID_1, self.DEGREE_OF_MASTERY_1) skill_services.create_user_skill_mastery(self.STUDENT_ID_2, self.SKILL_ID_2, self.DEGREE_OF_MASTERY_2)
def post(self, story_id, node_id): if not constants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES: raise self.PageNotFoundException try: story_fetchers.get_node_index_by_story_id_and_node_id( story_id, node_id) except Exception as e: raise self.PageNotFoundException(e) story = story_fetchers.get_story_by_id(story_id) completed_nodes = story_fetchers.get_completed_nodes_in_story( self.user_id, story_id) completed_node_ids = [ completed_node.id for completed_node in completed_nodes ] ordered_nodes = [ node for node in story.story_contents.get_ordered_nodes() ] next_exp_ids = [] next_node_id = None if not node_id in completed_node_ids: story_services.record_completed_node_in_story_context( self.user_id, story_id, node_id) completed_nodes = story_fetchers.get_completed_nodes_in_story( self.user_id, story_id) completed_node_ids = [ completed_node.id for completed_node in completed_nodes ] for node in ordered_nodes: if node.id not in completed_node_ids: next_exp_ids = [node.exploration_id] next_node_id = node.id break ready_for_review_test = False exp_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( next_exp_ids)) # If there are no questions for any of the acquired skills that the # learner has completed, do not show review tests. acquired_skills = skill_fetchers.get_multi_skills( story.get_acquired_skill_ids_for_node_ids(completed_node_ids)) acquired_skill_ids = [skill.id for skill in acquired_skills] questions_available = len( question_services.get_questions_by_skill_ids( 1, acquired_skill_ids, False)) > 0 learner_completed_story = len(completed_nodes) == len(ordered_nodes) learner_at_review_point_in_story = ( len(exp_summaries) != 0 and (len(completed_nodes) & constants.NUM_EXPLORATIONS_PER_REVIEW_TEST == 0)) if questions_available and (learner_at_review_point_in_story or learner_completed_story): ready_for_review_test = True return self.render_json({ 'summaries': exp_summaries, 'ready_for_review_test': ready_for_review_test, 'next_node_id': next_node_id })
def test_mark_topic_as_learnt_and_story_as_completed(self): self.NEW_USER_EMAIL = '*****@*****.**' self.NEW_USER_USERNAME = '******' self.save_new_valid_exploration(self.EXP_ID_3, self.admin_id, title='Title 3', end_state_name='End', correctness_feedback_enabled=True) self.publish_exploration(self.admin_id, self.EXP_ID_3) story = story_domain.Story.create_default_story( self.NEW_STORY_ID, 'Title', 'Description', self.TOPIC_ID, self.STORY_URL_FRAGMENT_TWO) story.meta_tag_content = 'story meta content' exp_summary_dicts = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_3], user=self.admin)) self.node_1 = { 'id': self.NODE_ID_1, 'title': 'Title 1', 'description': 'Description 1', 'thumbnail_filename': 'image_1.svg', 'thumbnail_bg_color': constants.ALLOWED_THUMBNAIL_BG_COLORS['chapter'][0], 'thumbnail_size_in_bytes': 21131, 'destination_node_ids': [], 'acquired_skill_ids': [], 'prerequisite_skill_ids': [], 'outline': '', 'outline_is_finalized': False, 'exploration_id': self.EXP_ID_3, 'exp_summary_dict': exp_summary_dicts[0], 'completed': False } story.story_contents.nodes = [ story_domain.StoryNode.from_dict(self.node_1) ] self.nodes = story.story_contents.nodes story.story_contents.initial_node_id = 'node_1' story.story_contents.next_node_id = 'node_2' story_services.save_new_story(self.admin_id, story) topic_services.add_canonical_story(self.admin_id, self.TOPIC_ID, self.NEW_STORY_ID) topic_services.publish_story(self.TOPIC_ID, self.NEW_STORY_ID, self.admin_id) csrf_token = self.get_new_csrf_token() story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_2) story_services.record_completed_node_in_story_context( self.viewer_id, self.STORY_ID, self.NODE_ID_1) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_VIEWER_UPDATES', True): self.post_json('%s/staging/topic/%s/%s' % (feconf.STORY_PROGRESS_URL_PREFIX, self.STORY_URL_FRAGMENT, self.NODE_ID_3), {}, csrf_token=csrf_token) self.assertEqual( len( learner_progress_services.get_all_learnt_topic_ids( self.viewer_id)), 1) self.assertEqual( len( learner_progress_services.get_all_completed_story_ids( self.viewer_id)), 1) def _mock_none_function(_): """Mocks None.""" return None story_fetchers_swap = self.swap(story_fetchers, 'get_story_by_id', _mock_none_function) with story_fetchers_swap: with self.capture_logging( min_level=logging.ERROR) as captured_logs: self.post_json('%s/staging/topic/%s/%s' % (feconf.STORY_PROGRESS_URL_PREFIX, self.STORY_URL_FRAGMENT, self.NODE_ID_3), {}, csrf_token=csrf_token) self.assertEqual(captured_logs, [ 'Could not find a story corresponding to %s ' 'id.' % self.STORY_ID ])