def test_replace_skill_id_for_all_questions(self): question_id_2 = question_services.get_new_question_id() self.save_new_question(question_id_2, self.editor_id, self._create_valid_question_data('ABC'), ['skill_1']) question_id_3 = question_services.get_new_question_id() self.save_new_question(question_id_3, self.editor_id, self._create_valid_question_data('ABC'), ['skill_2']) question_services.create_new_question_skill_link( self.editor_id, self.question_id, 'skill_1', 0.5) question_services.create_new_question_skill_link( self.editor_id, question_id_2, 'skill_1', 0.3) question_services.create_new_question_skill_link( self.editor_id, question_id_3, 'skill_2', 0.9) question_skill_links = ( question_services.get_question_skill_links_of_skill( 'skill_1', 'Skill Description 1')) self.assertEqual(len(question_skill_links), 2) question_ids = [ question_skill.question_id for question_skill in question_skill_links ] self.assertItemsEqual(question_ids, [self.question_id, question_id_2]) for question_skill in question_skill_links: if question_skill.question_id == self.question_id: self.assertEqual(question_skill.skill_difficulty, 0.5) question_services.replace_skill_id_for_all_questions( 'skill_1', 'Description 1', 'skill_3') question_skill_links = ( question_services.get_question_skill_links_of_skill( 'skill_1', 'Description 1')) self.assertEqual(len(question_skill_links), 0) question_skill_links = ( question_services.get_question_skill_links_of_skill( 'skill_3', 'Skill Description 3')) question_ids = [ question_skill.question_id for question_skill in question_skill_links ] self.assertItemsEqual(question_ids, [self.question_id, question_id_2]) for question_skill in question_skill_links: if question_skill.question_id == self.question_id: self.assertEqual(question_skill.skill_difficulty, 0.5) questions = question_fetchers.get_questions_by_ids( [self.question_id, question_id_2, question_id_3]) for question in questions: if question.id in ([self.question_id, question_id_2]): self.assertItemsEqual(question.linked_skill_ids, ['skill_3']) else: self.assertItemsEqual(question.linked_skill_ids, ['skill_2'])
def untag_deleted_misconceptions( committer_id, skill_id, skill_description, deleted_skill_misconception_ids): """Untags deleted misconceptions from questions belonging to a skill with the provided skill_id. Args: committer_id: str. The id of the user who triggered the update. skill_id: str. The skill id. skill_description: str. The description of the skill. deleted_skill_misconception_ids: list(str). The skill misconception ids of deleted misconceptions. The list items take the form <skill_id>-<misconception_id>. """ question_skill_links = get_question_skill_links_of_skill( skill_id, skill_description) question_ids = [model.question_id for model in question_skill_links] questions = question_fetchers.get_questions_by_ids(question_ids) for question in questions: change_list = [] inapplicable_skill_misconception_ids = ( question.inapplicable_skill_misconception_ids) deleted_inapplicable_skill_misconception_ids = ( list( set(deleted_skill_misconception_ids) & set(inapplicable_skill_misconception_ids))) if deleted_inapplicable_skill_misconception_ids: new_inapplicable_skill_misconception_ids = ( utils.compute_list_difference( question.inapplicable_skill_misconception_ids, deleted_inapplicable_skill_misconception_ids)) change_list.append(question_domain.QuestionChange({ 'cmd': 'update_question_property', 'property_name': 'inapplicable_skill_misconception_ids', 'new_value': new_inapplicable_skill_misconception_ids, 'old_value': question.inapplicable_skill_misconception_ids })) old_question_state_data_dict = question.question_state_data.to_dict() answer_groups = ( list(question.question_state_data.interaction.answer_groups)) for answer_group in answer_groups: tagged_skill_misconception_id = ( answer_group.to_dict()['tagged_skill_misconception_id']) if (tagged_skill_misconception_id in deleted_skill_misconception_ids): answer_group.tagged_skill_misconception_id = None question.question_state_data.interaction.answer_groups = answer_groups change_list.append(question_domain.QuestionChange({ 'cmd': 'update_question_property', 'property_name': 'question_state_data', 'new_value': question.question_state_data.to_dict(), 'old_value': old_question_state_data_dict })) update_question( committer_id, question.id, change_list, 'Untagged deleted skill misconception ids.')
def untag_deleted_misconceptions(committer_id, skill_id, skill_description): """Untags deleted misconceptions from questions belonging to a skill with the provided skill_id. Args: committer_id: str. The id of the user who triggered the update. skill_id: str. The skill id. skill_description: str. The description of the skill. """ question_skill_links = get_question_skill_links_of_skill( skill_id, skill_description) question_ids = [model.question_id for model in question_skill_links] questions = question_fetchers.get_questions_by_ids(question_ids) skill = skill_fetchers.get_skill_by_id(skill_id) skill_misconception_ids = ( [ skill.generate_skill_misconception_id(misconception.id) for misconception in skill.misconceptions ] ) for question in questions: change_list = [] inapplicable_skill_misconception_ids = ( question.inapplicable_skill_misconception_ids) deleted_inapplicable_skill_misconception_ids = ( utils.compute_list_difference( inapplicable_skill_misconception_ids, skill_misconception_ids)) for misconception_id in deleted_inapplicable_skill_misconception_ids: old_misconception_ids = list( question.inapplicable_skill_misconception_ids) question.inapplicable_skill_misconception_ids.remove( misconception_id) change_list.append(question_domain.QuestionChange({ 'cmd': 'update_question_property', 'property_name': 'inapplicable_skill_misconception_ids', 'new_value': question.inapplicable_skill_misconception_ids, 'old_value': old_misconception_ids })) old_question_state_data_dict = question.question_state_data.to_dict() answer_groups = ( list(question.question_state_data.interaction.answer_groups)) for i in python_utils.RANGE(len(answer_groups)): tagged_skill_misconception_id = ( answer_groups[i].to_dict()['tagged_skill_misconception_id']) if tagged_skill_misconception_id not in skill_misconception_ids: answer_groups[i].tagged_skill_misconception_id = None question.question_state_data.interaction.answer_groups = answer_groups change_list.append(question_domain.QuestionChange({ 'cmd': 'update_question_property', 'property_name': 'question_state_data', 'new_value': question.question_state_data.to_dict(), 'old_value': old_question_state_data_dict })) update_question( committer_id, question.id, change_list, 'Untagged deleted misconception id.')
def test_get_questions_by_ids(self): question_id_2 = question_services.get_new_question_id() self.save_new_question(question_id_2, self.editor_id, self._create_valid_question_data('DEF'), ['skill_1']) questions = question_fetchers.get_questions_by_ids( [self.question_id, 'invalid_question_id', question_id_2]) self.assertEqual(len(questions), 3) self.assertEqual(questions[0].id, self.question_id) self.assertIsNone(questions[1]) self.assertEqual(questions[2].id, question_id_2)
def get_questions_by_skill_ids(total_question_count, skill_ids, require_medium_difficulty): """Returns constant number of questions linked to each given skill id. Args: total_question_count: int. The total number of questions to return. skill_ids: list(str). The IDs of the skills to which the questions should be linked. require_medium_difficulty: bool. Indicates whether the returned questions should be of medium difficulty. Returns: list(Question). The list containing an expected number of total_question_count questions linked to each given skill id. question count per skill will be total_question_count divided by length of skill_ids, and it will be rounded up if not evenly divisible. If not enough questions for one skill, simply return all questions linked to it. The order of questions will follow the order of given skill ids, and the order of questions for the same skill is random when require_medium_difficulty is false, otherwise the order is sorted by absolute value of the difference between skill difficulty and the medium difficulty. Raises: Exception. Question count is higher than the maximum limit. """ if total_question_count > feconf.MAX_QUESTIONS_FETCHABLE_AT_ONE_TIME: raise Exception( 'Question count is too high, please limit the question count to ' '%d.' % feconf.MAX_QUESTIONS_FETCHABLE_AT_ONE_TIME) if require_medium_difficulty: question_skill_link_models = ( question_models.QuestionSkillLinkModel. get_question_skill_links_based_on_difficulty_equidistributed_by_skill( # pylint: disable=line-too-long total_question_count, skill_ids, constants.SKILL_DIFFICULTY_LABEL_TO_FLOAT[ constants.SKILL_DIFFICULTY_MEDIUM])) else: question_skill_link_models = ( question_models.QuestionSkillLinkModel. get_question_skill_links_equidistributed_by_skill( total_question_count, skill_ids)) question_ids = [model.question_id for model in question_skill_link_models] questions = question_fetchers.get_questions_by_ids(question_ids) return questions
def test_get_questions_by_ids(self) -> None: question_id_2 = question_services.get_new_question_id( ) # type: ignore[no-untyped-call] self.save_new_question( # type: ignore[no-untyped-call] question_id_2, self.editor_id, self._create_valid_question_data('DEF'), ['skill_1']) # type: ignore[no-untyped-call] questions = question_fetchers.get_questions_by_ids( [self.question_id, 'invalid_question_id', question_id_2]) self.assertEqual(len(questions), 3) # Ruling out the possibility of None for mypy type checking. assert questions[0] is not None self.assertEqual(questions[0].id, self.question_id) self.assertIsNone(questions[1]) # Ruling out the possibility of None for mypy type checking. assert questions[2] is not None self.assertEqual(questions[2].id, question_id_2)