def test_get_skill_descriptions_by_ids(self): self.save_new_skill( 'skill_2', self.USER_ID, 'Description 2', misconceptions=[], skill_contents=skill_domain.SkillContents( state_domain.SubtitledHtml('1', '<p>Explanation</p>'), [state_domain.SubtitledHtml('2', '<p>Example 1</p>')], state_domain.RecordedVoiceovers.from_dict( {'voiceovers_mapping': { '1': {}, '2': {} }}), state_domain.WrittenTranslations.from_dict( {'translations_mapping': { '1': {}, '2': {} }}))) self.save_new_skill( 'skill_3', self.USER_ID, 'Description 3', misconceptions=[], skill_contents=skill_domain.SkillContents( state_domain.SubtitledHtml('1', '<p>Explanation</p>'), [state_domain.SubtitledHtml('2', '<p>Example 1</p>')], state_domain.RecordedVoiceovers.from_dict( {'voiceovers_mapping': { '1': {}, '2': {} }}), state_domain.WrittenTranslations.from_dict( {'translations_mapping': { '1': {}, '2': {} }}))) with self.swap(feconf, 'CAN_SEND_EMAILS', True): skill_descriptions = skill_services.get_skill_descriptions_by_ids( 'topic_id', [self.SKILL_ID, 'skill_2', 'skill_3']) messages = self.mail_stub.get_sent_messages( to=feconf.ADMIN_EMAIL_ADDRESS) self.assertEqual(len(messages), 0) skill_services.delete_skill(self.USER_ID, 'skill_2') skill_descriptions = skill_services.get_skill_descriptions_by_ids( 'topic_id', [self.SKILL_ID, 'skill_2', 'skill_3']) messages = self.mail_stub.get_sent_messages( to=feconf.ADMIN_EMAIL_ADDRESS) expected_email_html_body = ('The deleted skills: skill_2 are still' ' present in topic with id topic_id') self.assertEqual(len(messages), 1) self.assertIn(expected_email_html_body, messages[0].html.decode()) self.assertEqual( skill_descriptions, { self.SKILL_ID: 'Description', 'skill_2': None, 'skill_3': 'Description 3' })
def test_job_skips_deleted_skills(self): rubrics = [ skill_domain.Rubric( constants.SKILL_DIFFICULTIES[0], ['<p>Explanation 1</p>']), skill_domain.Rubric( constants.SKILL_DIFFICULTIES[1], ['<p>Explanation 2</p>']), skill_domain.Rubric( constants.SKILL_DIFFICULTIES[2], ['<p>Explanation 3</p>'])] skill = skill_domain.Skill.create_default_skill( 'valid_skill', 'A description', rubrics) skill_services.save_new_skill(self.albert_id, skill) skill_services.delete_skill( self.albert_id, skill.id) with self.assertRaisesRegexp(Exception, 'Entity .* not found'): skill_fetchers.get_skill_by_id(skill.id) job_id = ( skill_jobs_one_off.SkillMathRteAuditOneOffJob.create_new()) skill_jobs_one_off.SkillMathRteAuditOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() output = skill_jobs_one_off.SkillMathRteAuditOneOffJob.get_output( job_id) self.assertEqual(output, [])
def test_get_descriptions_of_skills(self): self.save_new_skill( 'skill_id_1', self.user_id_admin, description='Description 1', misconceptions=[], skill_contents=skill_domain.SkillContents( state_domain.SubtitledHtml('1', '<p>Explanation</p>'), [ state_domain.SubtitledHtml('2', '<p>Example 1</p>')], state_domain.RecordedVoiceovers.from_dict( {'voiceovers_mapping': {'1': {}, '2': {}}}), state_domain.WrittenTranslations.from_dict( {'translations_mapping': {'1': {}, '2': {}}}))) self.save_new_skill( 'skill_id_2', self.user_id_admin, description='Description 2', misconceptions=[], skill_contents=skill_domain.SkillContents( state_domain.SubtitledHtml('1', '<p>Explanation</p>'), [ state_domain.SubtitledHtml('2', '<p>Example 1</p>')], state_domain.RecordedVoiceovers.from_dict( {'voiceovers_mapping': {'1': {}, '2': {}}}), state_domain.WrittenTranslations.from_dict( {'translations_mapping': {'1': {}, '2': {}}}))) skill_services.delete_skill(self.user_id_admin, 'skill_id_2') skill_descriptions, deleted_skill_ids = ( skill_services.get_descriptions_of_skills( ['skill_id_1', 'skill_id_2'])) self.assertEqual(deleted_skill_ids, ['skill_id_2']) self.assertEqual( skill_descriptions, { 'skill_id_1': 'Description 1', 'skill_id_2': None } )
def test_migration_job_skips_deleted_skill(self): """Tests that the skill migration job skips deleted skill and does not attempt to migrate. """ skill = skill_domain.Skill.create_default_skill( self.SKILL_ID, 'A description', self.rubrics) skill_services.save_new_skill(self.albert_id, skill) # Delete the skill before migration occurs. skill_services.delete_skill(self.albert_id, self.SKILL_ID) # Ensure the skill is deleted. with self.assertRaisesRegexp(Exception, 'Entity .* not found'): skill_fetchers.get_skill_by_id(self.SKILL_ID) # Start migration job on sample skill. job_id = (skill_jobs_one_off.SkillMigrationOneOffJob.create_new()) skill_jobs_one_off.SkillMigrationOneOffJob.enqueue(job_id) # This running without errors indicates the deleted skill is # being ignored. self.process_and_flush_pending_mapreduce_tasks() # Ensure the skill is still deleted. with self.assertRaisesRegexp(Exception, 'Entity .* not found'): skill_fetchers.get_skill_by_id(self.SKILL_ID) output = skill_jobs_one_off.SkillMigrationOneOffJob.get_output(job_id) expected = [[u'skill_deleted', [u'Encountered 1 deleted skills.']]] self.assertEqual(expected, [ast.literal_eval(x) for x in output])
def post(self): """Handles the POST request.""" old_skill_id = self.payload.get('old_skill_id') new_skill_id = self.payload.get('new_skill_id') new_skill = skill_fetchers.get_skill_by_id(new_skill_id, strict=False) if new_skill is None: raise self.PageNotFoundException( Exception('The new skill with the given id doesn\'t exist.')) old_skill = skill_fetchers.get_skill_by_id(old_skill_id, strict=False) if old_skill is None: raise self.PageNotFoundException( Exception('The old skill with the given id doesn\'t exist.')) skill_services.replace_skill_id_in_all_topics(self.user_id, old_skill_id, new_skill_id) question_services.replace_skill_id_for_all_questions( old_skill_id, old_skill.description, new_skill_id) changelist = [ skill_domain.SkillChange({ 'cmd': skill_domain.CMD_UPDATE_SKILL_PROPERTY, 'property_name': (skill_domain.SKILL_PROPERTY_SUPERSEDING_SKILL_ID), 'old_value': old_skill.superseding_skill_id, 'new_value': new_skill_id }) ] skill_services.update_skill( self.user_id, old_skill_id, changelist, 'Marking the skill as having being merged successfully.') skill_services.delete_skill(self.user_id, old_skill_id) self.render_json({'merged_into_skill': new_skill_id})
def test_delete_skill(self): skill_services.delete_skill(self.USER_ID, self.SKILL_ID) self.assertEqual( skill_services.get_skill_by_id(self.SKILL_ID, strict=False), None) self.assertEqual( skill_services.get_skill_summary_by_id(self.SKILL_ID, strict=False), None)
def test_get_skill_descriptions_by_ids(self): self.save_new_skill('skill_2', self.USER_ID, 'Description 2', misconceptions=[], skill_contents=skill_domain.SkillContents( 'Explanation', ['Example 1'])) self.save_new_skill('skill_3', self.USER_ID, 'Description 3', misconceptions=[], skill_contents=skill_domain.SkillContents( 'Explanation', ['Example 1'])) with self.swap(feconf, 'CAN_SEND_EMAILS', True): skill_descriptions = skill_services.get_skill_descriptions_by_ids( 'topic_id', [self.SKILL_ID, 'skill_2', 'skill_3']) messages = self.mail_stub.get_sent_messages( to=feconf.ADMIN_EMAIL_ADDRESS) self.assertEqual(len(messages), 0) skill_services.delete_skill(self.USER_ID, 'skill_2') skill_descriptions = skill_services.get_skill_descriptions_by_ids( 'topic_id', [self.SKILL_ID, 'skill_2', 'skill_3']) messages = self.mail_stub.get_sent_messages( to=feconf.ADMIN_EMAIL_ADDRESS) expected_email_html_body = ('The deleted skills: skill_2 are still' ' present in topic with id topic_id') self.assertEqual(len(messages), 1) self.assertIn(expected_email_html_body, messages[0].html.decode()) self.assertEqual( skill_descriptions, { self.SKILL_ID: 'Description', 'skill_2': None, 'skill_3': 'Description 3' })
def delete(self, skill_id): """Handles Delete requests.""" if not feconf.ENABLE_NEW_STRUCTURES: raise self.PageNotFoundException skill_domain.Skill.require_valid_skill_id(skill_id) if not skill_id: raise self.PageNotFoundException skill_services.delete_skill(self.user_id, skill_id)
def test_get_with_user_logged_in(self): skill_services.delete_skill(self.admin_id, self.skill_id_1) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True): self.login(self.NEW_USER_EMAIL) with self.swap(feconf, 'CAN_SEND_EMAILS', True): messages = self.mail_stub.get_sent_messages( to=feconf.ADMIN_EMAIL_ADDRESS) self.assertEqual(len(messages), 0) json_response = self.get_json( '%s/%s' % (feconf.TOPIC_DATA_HANDLER, 'public_topic_name')) messages = self.mail_stub.get_sent_messages( to=feconf.ADMIN_EMAIL_ADDRESS) expected_email_html_body = ('The deleted skills: %s are still' ' present in topic with id %s' % (self.skill_id_1, self.topic_id)) self.assertEqual(len(messages), 1) self.assertIn(expected_email_html_body, messages[0].html.decode()) expected_dict = { 'topic_name': 'public_topic_name', 'topic_id': self.topic_id, 'canonical_story_dicts': [{ 'id': self.story_1.id, 'title': self.story_1.title, 'description': self.story_1.description, 'node_count': self.story_1.node_count, 'published': True }], 'additional_story_dicts': [{ 'id': self.story_2.id, 'title': self.story_2.title, 'description': self.story_2.description, 'node_count': self.story_2.node_count, 'published': True }], 'uncategorized_skill_ids': [self.skill_id_1], 'subtopics': [{ u'skill_ids': [self.skill_id_2], u'id': 1, u'title': u'subtopic_name' }], 'degrees_of_mastery': { self.skill_id_1: 0.3, self.skill_id_2: 0.5 }, 'skill_descriptions': { self.skill_id_1: None, self.skill_id_2: 'Skill Description 2' }, 'train_tab_should_be_displayed': False } self.assertDictContainsSubset(expected_dict, json_response) self.logout()
def test_get_rubrics_of_linked_skills(self): self.save_new_skill( 'skill_id_1', self.user_id_admin, 'Description 1', misconceptions=[], skill_contents=skill_domain.SkillContents( state_domain.SubtitledHtml('1', '<p>Explanation</p>'), [state_domain.SubtitledHtml('2', '<p>Example 1</p>')], state_domain.RecordedVoiceovers.from_dict( {'voiceovers_mapping': { '1': {}, '2': {} }}), state_domain.WrittenTranslations.from_dict( {'translations_mapping': { '1': {}, '2': {} }}))) self.save_new_skill( 'skill_id_2', self.user_id_admin, 'Description 2', misconceptions=[], skill_contents=skill_domain.SkillContents( state_domain.SubtitledHtml('1', '<p>Explanation</p>'), [state_domain.SubtitledHtml('2', '<p>Example 1</p>')], state_domain.RecordedVoiceovers.from_dict( {'voiceovers_mapping': { '1': {}, '2': {} }}), state_domain.WrittenTranslations.from_dict( {'translations_mapping': { '1': {}, '2': {} }}))) skill_services.delete_skill(self.user_id_admin, 'skill_id_2') skill_rubrics, deleted_skill_ids = ( skill_services.get_rubrics_of_skills(['skill_id_1', 'skill_id_2'])) self.assertEqual(deleted_skill_ids, ['skill_id_2']) self.assertEqual( skill_rubrics, { 'skill_id_1': [ skill_domain.Rubric(constants.SKILL_DIFFICULTIES[0], 'Explanation 1').to_dict(), skill_domain.Rubric(constants.SKILL_DIFFICULTIES[1], 'Explanation 2').to_dict(), skill_domain.Rubric(constants.SKILL_DIFFICULTIES[2], 'Explanation 3').to_dict() ], 'skill_id_2': None })
def test_editable_topic_handler_get(self): skill_services.delete_skill(self.admin_id, self.skill_id_2) # Check that non-admins cannot access the editable topic data. self.login(self.NEW_USER_EMAIL) self.get_json( '%s/%s' % ( feconf.TOPIC_EDITOR_DATA_URL_PREFIX, self.topic_id), expected_status_int=401) self.logout() # Check that admins can access the editable topic data. self.login(self.ADMIN_EMAIL) with self.swap(feconf, 'CAN_SEND_EMAILS', True): messages = self._get_sent_email_messages( feconf.ADMIN_EMAIL_ADDRESS) self.assertEqual(len(messages), 0) json_response = self.get_json( '%s/%s' % ( feconf.TOPIC_EDITOR_DATA_URL_PREFIX, self.topic_id)) self.assertEqual(self.topic_id, json_response['topic_dict']['id']) self.assertTrue( self.skill_id in json_response['skill_question_count_dict']) self.assertEqual( json_response['skill_question_count_dict'][self.skill_id], 0) self.assertTrue( self.skill_id_2 in json_response['skill_question_count_dict']) self.assertEqual( json_response['skill_question_count_dict'][self.skill_id_2], 0) self.assertEqual( 'Skill Description', json_response['skill_id_to_description_dict'][self.skill_id]) messages = self._get_sent_email_messages( feconf.ADMIN_EMAIL_ADDRESS) expected_email_html_body = ( 'The deleted skills: %s are still' ' present in topic with id %s' % ( self.skill_id_2, self.topic_id)) self.assertEqual(len(messages), 1) self.assertIn( expected_email_html_body, messages[0].html.decode()) self.logout() # Check that editable topic handler is accessed only when a topic id # passed has an associated topic. self.login(self.ADMIN_EMAIL) self.get_json( '%s/%s' % ( feconf.TOPIC_EDITOR_DATA_URL_PREFIX, topic_services.get_new_topic_id()), expected_status_int=404) self.logout()
def test_delete_skill_deletes_skill_opportunity(self): self.save_new_skill(self.SKILL_ID, self.USER_ID, 'skill_description') skill_opportunities, _, _ = ( opportunity_services.get_skill_opportunities(None)) self.assertEqual(len(skill_opportunities), 1) skill_services.delete_skill(self.USER_ID, self.SKILL_ID) skill_opportunities, _, _ = ( opportunity_services.get_skill_opportunities(None)) self.assertEqual(len(skill_opportunities), 0)
def delete(self, skill_id): """Handles Delete requests.""" skill_domain.Skill.require_valid_skill_id(skill_id) if skill_services.skill_has_associated_questions(skill_id): raise Exception( 'Please delete all questions associated with this skill ' 'first.') skill_services.delete_skill(self.user_id, skill_id) self.render_json(self.values)
def delete(self, skill_id): """Handles Delete requests.""" skill_services.remove_skill_from_all_topics(self.user_id, skill_id) if skill_services.skill_has_associated_questions(skill_id): raise self.InvalidInputException( 'Please delete all questions associated with this skill ' 'first.') skill_services.delete_skill(self.user_id, skill_id) self.render_json(self.values)
def delete(self, skill_id): """Handles Delete requests.""" if not constants.ENABLE_NEW_STRUCTURE_EDITORS: raise self.PageNotFoundException skill_domain.Skill.require_valid_skill_id(skill_id) if skill_services.skill_has_associated_questions(skill_id): raise Exception( 'Please delete all questions associated with this skill ' 'first.') skill_services.delete_skill(self.user_id, skill_id) self.render_json(self.values)
def delete(self, skill_id): """Handles Delete requests.""" skill_domain.Skill.require_valid_skill_id(skill_id) skill_ids_assigned_to_some_topic = ( topic_services.get_all_skill_ids_assigned_to_some_topic()) if skill_id in skill_ids_assigned_to_some_topic: raise self.InvalidInputException( 'Cannot delete skill that is assigned to a topic.') if skill_services.skill_has_associated_questions(skill_id): raise self.InvalidInputException( 'Please delete all questions associated with this skill ' 'first.') skill_services.delete_skill(self.user_id, skill_id) self.render_json(self.values)
def test_model_with_last_updated_greater_than_current_time(self): skill_services.delete_skill(self.owner_id, '1') skill_services.delete_skill(self.owner_id, '2') expected_output = [ (u'[u\'failed validation check for current time check of ' 'SkillSummaryModel\', ' '[u\'Entity id %s: The last_updated field has a ' 'value %s which is greater than the time when the job was run\']]' ) % (self.model_instance_0.id, self.model_instance_0.last_updated) ] mocked_datetime = datetime.datetime.utcnow() - datetime.timedelta( hours=13) with datastore_services.mock_datetime_for_datastore(mocked_datetime): self.run_job_and_check_output(expected_output, sort=True, literal_eval=False)
def test_regeneration_job_for_deleted_skill_returns_empty_list_output(self): skill_opp_model_regen_job_class = ( opportunity_jobs_one_off.SkillOpportunityModelRegenerationJob) skill_services.delete_skill(self.admin_id, self.skill_id_1) skill_services.delete_skill(self.admin_id, self.skill_id_2) job_id = skill_opp_model_regen_job_class.create_new() skill_opp_model_regen_job_class.enqueue(job_id) self.assertEqual( self.count_jobs_in_taskqueue( taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 1) self.process_and_flush_pending_tasks() output = skill_opp_model_regen_job_class.get_output(job_id) self.assertEqual(output, [])
def test_editable_topic_handler_put(self): # Check that admins can edit a topic. change_cmd = { 'version': 2, 'commit_message': 'Some changes and added a subtopic.', 'topic_and_subtopic_page_change_dicts': [{ 'cmd': 'update_topic_property', 'property_name': 'name', 'old_value': '', 'new_value': 'A new name' }, { 'cmd': 'update_subtopic_page_property', 'property_name': 'page_contents_html', 'old_value': { 'html': '', 'content_id': 'content' }, 'subtopic_id': 1, 'new_value': { 'html': '<p>New Data</p>', 'content_id': 'content' } }, { 'cmd': 'update_subtopic_property', 'property_name': 'url_fragment', 'new_value': 'subtopic-one', 'old_value': '', 'subtopic_id': 1 }, { 'cmd': 'add_subtopic', 'subtopic_id': 2, 'title': 'Title2' }, { 'cmd': 'update_subtopic_property', 'property_name': 'url_fragment', 'new_value': 'subtopic-two', 'old_value': '', 'subtopic_id': 2 }, { 'cmd': 'update_subtopic_page_property', 'property_name': 'page_contents_html', 'old_value': { 'html': '', 'content_id': 'content' }, 'new_value': { 'html': '<p>New Value</p>', 'content_id': 'content' }, 'subtopic_id': 2 }, { 'cmd': 'update_subtopic_page_property', 'property_name': 'page_contents_audio', 'old_value': { 'voiceovers_mapping': { 'content': {} } }, 'new_value': { 'voiceovers_mapping': { 'content': { 'en': { 'filename': 'test.mp3', 'file_size_bytes': 100, 'needs_update': False, 'duration_secs': 0.34342 } } } }, 'subtopic_id': 2 }] } self.login(self.CURRICULUM_ADMIN_EMAIL) csrf_token = self.get_new_csrf_token() skill_services.delete_skill(self.admin_id, self.skill_id_2) with self.swap(feconf, 'CAN_SEND_EMAILS', True): messages = self._get_sent_email_messages( feconf.ADMIN_EMAIL_ADDRESS) self.assertEqual(len(messages), 0) json_response = self.put_json( '%s/%s' % (feconf.TOPIC_EDITOR_DATA_URL_PREFIX, self.topic_id), change_cmd, csrf_token=csrf_token) self.assertEqual(self.topic_id, json_response['topic_dict']['id']) self.assertEqual('A new name', json_response['topic_dict']['name']) self.assertEqual(2, len(json_response['topic_dict']['subtopics'])) self.assertEqual( 'Skill Description', json_response['skill_id_to_description_dict'][self.skill_id]) messages = self._get_sent_email_messages( feconf.ADMIN_EMAIL_ADDRESS) expected_email_html_body = ('The deleted skills: %s are still' ' present in topic with id %s' % (self.skill_id_2, self.topic_id)) self.assertEqual(len(messages), 1) self.assertIn(expected_email_html_body, messages[0].html) # Test if the corresponding subtopic pages were created. json_response = self.get_json( '%s/%s/%s' % (feconf.SUBTOPIC_PAGE_EDITOR_DATA_URL_PREFIX, self.topic_id, 1)) self.assertEqual( { 'subtitled_html': { 'html': '<p>New Data</p>', 'content_id': 'content' }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': {} } }, 'written_translations': { 'translations_mapping': { 'content': {} } } }, json_response['subtopic_page']['page_contents']) json_response = self.get_json( '%s/%s/%s' % (feconf.SUBTOPIC_PAGE_EDITOR_DATA_URL_PREFIX, self.topic_id, 2)) self.assertEqual( { 'subtitled_html': { 'html': '<p>New Value</p>', 'content_id': 'content' }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': { 'en': { 'file_size_bytes': 100, 'filename': 'test.mp3', 'needs_update': False, 'duration_secs': 0.34342 } } } }, 'written_translations': { 'translations_mapping': { 'content': {} } } }, json_response['subtopic_page']['page_contents']) self.logout() # Test that any topic manager cannot edit the topic. self.login(self.TOPIC_MANAGER_EMAIL) self.put_json('%s/%s' % (feconf.TOPIC_EDITOR_DATA_URL_PREFIX, self.topic_id), change_cmd, csrf_token=csrf_token, expected_status_int=401) self.logout() # Check that non-admins and non-topic managers cannot edit a topic. self.put_json('%s/%s' % (feconf.TOPIC_EDITOR_DATA_URL_PREFIX, self.topic_id), change_cmd, csrf_token=csrf_token, expected_status_int=401) # Check that topic can not be edited when version is None. self.login(self.CURRICULUM_ADMIN_EMAIL) json_response = self.put_json( '%s/%s' % (feconf.TOPIC_EDITOR_DATA_URL_PREFIX, self.topic_id), {'version': None}, csrf_token=csrf_token, expected_status_int=400) self.assertEqual(json_response['error'], 'Invalid POST request: a version must be specified.') self.logout() # Check topic can not be edited when payload version differs from # topic version. self.login(self.CURRICULUM_ADMIN_EMAIL) topic_id_1 = topic_fetchers.get_new_topic_id() self.save_new_topic(topic_id_1, self.admin_id, name='Name 1', abbreviated_name='topic-three', url_fragment='topic-three', description='Description 1', canonical_story_ids=[], additional_story_ids=[], uncategorized_skill_ids=[self.skill_id], subtopics=[], next_subtopic_id=1) json_response = self.put_json( '%s/%s' % (feconf.TOPIC_EDITOR_DATA_URL_PREFIX, topic_id_1), {'version': '3'}, csrf_token=csrf_token, expected_status_int=400) self.assertEqual( json_response['error'], 'Trying to update version 1 of topic from version 3, ' 'which is too old. Please reload the page and try again.') self.logout()
def test_get_with_user_logged_in(self): skill_services.delete_skill(self.admin_id, self.skill_id_1) self.login(self.NEW_USER_EMAIL) with self.swap(feconf, 'CAN_SEND_EMAILS', True): messages = self._get_sent_email_messages( feconf.ADMIN_EMAIL_ADDRESS) self.assertEqual(len(messages), 0) json_response = self.get_json( '%s/staging/%s' % (feconf.TOPIC_DATA_HANDLER, 'public')) messages = self._get_sent_email_messages( feconf.ADMIN_EMAIL_ADDRESS) expected_email_html_body = ( 'The deleted skills: %s are still' ' present in topic with id %s' % ( self.skill_id_1, self.topic_id)) self.assertEqual(len(messages), 1) self.assertIn(expected_email_html_body, messages[0].html) expected_dict = { 'topic_name': 'public_topic_name', 'topic_id': self.topic_id, 'canonical_story_dicts': [{ 'id': self.story_1.id, 'title': self.story_1.title, 'description': self.story_1.description, 'node_titles': self.story_1.node_titles, 'thumbnail_filename': None, 'thumbnail_bg_color': None, 'story_is_published': True, 'completed_node_titles': [], 'url_fragment': 'story-frag-one', 'all_node_dicts': [] }], 'additional_story_dicts': [{ 'id': self.story_2.id, 'title': self.story_2.title, 'description': self.story_2.description, 'node_titles': self.story_2.node_titles, 'thumbnail_filename': None, 'thumbnail_bg_color': None, 'story_is_published': True, 'completed_node_titles': [], 'url_fragment': 'story-frag-two', 'all_node_dicts': [] }], 'uncategorized_skill_ids': [self.skill_id_1], 'subtopics': [{ u'thumbnail_filename': u'image.svg', u'thumbnail_bg_color': u'#FFFFFF', u'thumbnail_size_in_bytes': 21131, u'skill_ids': [self.skill_id_2], u'id': 1, u'title': u'subtopic_name', u'url_fragment': u'subtopic-name'}], 'degrees_of_mastery': { self.skill_id_1: 0.3, self.skill_id_2: 0.5 }, 'skill_descriptions': { self.skill_id_1: None, self.skill_id_2: 'Skill Description 2' }, 'practice_tab_is_displayed': False } self.assertDictContainsSubset(expected_dict, json_response) self.logout()