diff --git a/core/controllers/acl_decorators_test.py b/core/controllers/acl_decorators_test.py index c794f7756375..d971451ccc41 100644 --- a/core/controllers/acl_decorators_test.py +++ b/core/controllers/acl_decorators_test.py @@ -2482,8 +2482,6 @@ class CanManageContributorsRoleDecoratorTests(test_utils.GenericTestBase): username = 'user' user_email = 'user@example.com' - QUESTION_ADMIN_EMAIL: Final = 'questionExpert@app.com' - QUESTION_ADMIN_USERNAME: Final = 'questionExpert' TRANSLATION_ADMIN_EMAIL: Final = 'translatorExpert@app.com' TRANSLATION_ADMIN_USERNAME: Final = 'translationExpert' diff --git a/core/controllers/admin.py b/core/controllers/admin.py index 4e810d487ad4..f7b51b661441 100644 --- a/core/controllers/admin.py +++ b/core/controllers/admin.py @@ -56,6 +56,7 @@ from core.domain import story_services from core.domain import subtopic_page_domain from core.domain import subtopic_page_services +from core.domain import suggestion_services from core.domain import topic_domain from core.domain import topic_fetchers from core.domain import topic_services @@ -223,6 +224,8 @@ class AdminHandlerNormalizePayloadDict(TypedDict): collection_id: Optional[str] num_dummy_exps_to_generate: Optional[int] num_dummy_exps_to_publish: Optional[int] + num_dummy_question_suggestions_generate: Optional[int] + skill_id: Optional[str] num_dummy_translation_opportunities_to_generate: Optional[int] data: Optional[str] topic_id: Optional[str] @@ -256,6 +259,7 @@ class AdminHandler( 'generate_dummy_new_skill_data', 'generate_dummy_blog_post', 'generate_dummy_classroom', + 'generate_dummy_question_suggestions', 'upload_topic_similarities', 'regenerate_topic_related_opportunities', 'update_platform_parameter_rules', @@ -285,6 +289,18 @@ class AdminHandler( }, 'default_value': None }, + 'skill_id': { + 'schema': { + 'type': 'basestring' + }, + 'default_value': None + }, + 'num_dummy_question_suggestions_generate': { + 'schema': { + 'type': 'int' + }, + 'default_value': None + }, 'num_dummy_exps_to_publish': { 'schema': { 'type': 'int' @@ -363,6 +379,9 @@ class AdminHandler( @acl_decorators.can_access_admin_page def get(self) -> None: """Populates the data on the admin page.""" + skill_summaries = skill_services.get_all_skill_summaries() + skill_summary_dicts = [ + summary.to_dict() for summary in skill_summaries] demo_exploration_ids = list(feconf.DEMO_EXPLORATIONS.keys()) topic_summaries = topic_fetchers.get_all_topic_summaries() @@ -394,6 +413,7 @@ def get(self) -> None: 'role_to_actions': role_services.get_role_actions(), 'topic_summaries': topic_summary_dicts, 'platform_params_dicts': platform_params_dicts, + 'skill_list': skill_summary_dicts, }) @acl_decorators.can_access_admin_page @@ -428,6 +448,10 @@ def post(self) -> None: Exception. The commit_message must be provided when the action is update_platform_parameter_rules. InvalidInputException. The input provided is not valid. + Exception. The skill_id must be provided when + the action is generate_dummy_question_suggestions. + Exception. The num_dummy_question_suggestions_generate must be + provided when the action is generate_dummy_question_suggestions. """ assert self.user_id is not None assert self.normalized_payload is not None @@ -505,6 +529,25 @@ def post(self) -> None: self._generate_dummy_skill_and_questions() elif action == 'generate_dummy_classroom': self._generate_dummy_classroom() + elif action == 'generate_dummy_question_suggestions': + skill_id = self.normalized_payload.get('skill_id') + if skill_id is None: + raise Exception( + 'The \'skill_id\' must be provided when' + ' the action is _generate_dummy_question_suggestions.' + ) + num_dummy_question_suggestions_generate = ( + self.normalized_payload.get( + 'num_dummy_question_suggestions_generate') + ) + if num_dummy_question_suggestions_generate is None: + raise Exception( + 'The \'num_dummy_question_suggestions_generate\' must' + ' be provided when the action is ' + '_generate_dummy_question_suggestions.' + ) + self._generate_dummy_question_suggestions( + skill_id, num_dummy_question_suggestions_generate) elif action == 'upload_topic_similarities': data = self.normalized_payload.get('data') if data is None: @@ -1541,6 +1584,117 @@ def _generate_dummy_classroom(self) -> None: else: raise Exception('Cannot generate dummy classroom in production.') + def _generate_dummy_question_suggestions( + self, skill_id: str, + num_dummy_question_suggestions_generate: int) -> None: + """Generates and loads the database with a specified number of + suggestion question for the selected skill. + + Raises: + Exception. Cannot load suggestion questions in production mode. + Exception. User does not have enough rights to generate data. + """ + assert self.user_id is not None + if constants.DEV_MODE: + if ((feconf.ROLE_ID_QUESTION_ADMIN not in self.user.roles) + and (not user_services.can_submit_question_suggestions( + self.user_id))): + raise Exception(( + 'User \'%s\' must be a question submitter or question admin' + ' in order to generate question suggestions.' + ) % self.username) + for _ in range(num_dummy_question_suggestions_generate): + content_id_generator = translation_domain.ContentIdGenerator() + content_id_generator.generate( + translation_domain.ContentType.CONTENT) + content_id_generator.generate( + translation_domain.ContentType.DEFAULT_OUTCOME) + state = state_domain.State.create_default_state( + 'default_state', + content_id_generator.generate( + translation_domain.ContentType.CONTENT), + content_id_generator.generate( + translation_domain.ContentType.DEFAULT_OUTCOME), + is_initial_state=True) + state.update_interaction_id('TextInput') + solution_dict: state_domain.SolutionDict = { + 'answer_is_exclusive': False, + 'correct_answer': 'Solution', + 'explanation': { + 'content_id': content_id_generator.generate( + translation_domain.ContentType.SOLUTION), + 'html': '

This is a solution.

', + }, + } + hints_list = [ + state_domain.Hint( + state_domain.SubtitledHtml( + content_id_generator.generate( + translation_domain.ContentType.HINT), + '

This is a hint.

')), + ] + # Ruling out None for mypy type checking, + # as interaction_id is already updated. + assert state.interaction.id is not None + solution = state_domain.Solution.from_dict( + state.interaction.id, solution_dict) + state.update_interaction_solution(solution) + state.update_interaction_hints(hints_list) + state.update_interaction_customization_args({ + 'placeholder': { + 'value': { + 'content_id': content_id_generator.generate( + translation_domain.ContentType.CUSTOMIZATION_ARG, # pylint: disable=line-too-long + extra_prefix='placeholder'), + 'unicode_str': 'Enter text here', + }, + }, + 'rows': {'value': 1}, + 'catchMisspellings': {'value': False} + }) + # Here, state is a State domain object and it is created using + # 'create_default_state' method. So, 'state' is a default_state + # and it is always going to contain a default_outcome. Thus to + # narrow down the type from Optional[Outcome] to Outcome for + # default_outcome, we used assert here. + assert state.interaction.default_outcome is not None + state.interaction.default_outcome.labelled_as_correct = True + state.interaction.default_outcome.dest = None + suggestion_change: Dict[ + str, Union[str, float, question_domain.QuestionDict] + ] = { + 'cmd': ( + question_domain + .CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION), + 'question_dict': { + 'id': '', + 'version': 0, + 'question_state_data': state.to_dict(), + 'language_code': 'en', + 'question_state_data_schema_version': 1, + 'linked_skill_ids': [skill_id], + 'inapplicable_skill_misconception_ids': [], + 'next_content_id_index': ( + content_id_generator.next_content_id_index) + }, + 'skill_id': skill_id, + 'skill_difficulty': 0.3 + } + + suggestion = suggestion_services.create_suggestion( + feconf.SUGGESTION_TYPE_ADD_QUESTION, + feconf.ENTITY_TYPE_SKILL, + skill_id, 1, + self.user_id, suggestion_change, 'test description') + + ( + suggestion_services + .update_question_contribution_stats_at_submission( + suggestion)) + else: + raise Exception( + 'Cannot generate dummy question suggestion in production.') + class AdminRoleHandlerNormalizedGetRequestDict(TypedDict): """Dict representation of AdminRoleHandler's GET normalized_request diff --git a/core/controllers/admin_test.py b/core/controllers/admin_test.py index e17f69ce6b6b..b00c5952333a 100644 --- a/core/controllers/admin_test.py +++ b/core/controllers/admin_test.py @@ -45,6 +45,7 @@ from core.domain import story_domain from core.domain import story_fetchers from core.domain import story_services +from core.domain import suggestion_services from core.domain import topic_domain from core.domain import topic_fetchers from core.domain import topic_services @@ -235,6 +236,48 @@ def test_without_num_dummy_exps_to_publish_action_is_not_performed( self.logout() + def test_without_skill_id_dummy_question_suggestions_action_is_not_performed( # pylint: disable=line-too-long + self + ) -> None: + self.login(self.CURRICULUM_ADMIN_EMAIL, is_super_admin=True) + csrf_token = self.get_new_csrf_token() + + assert_raises_regexp_context_manager = self.assertRaisesRegex( + Exception, + 'The \'skill_id\' must be provided when the ' + 'action is _generate_dummy_question_suggestions.' + ) + with assert_raises_regexp_context_manager, self.prod_mode_swap: + self.post_json( + '/adminhandler', { + 'action': 'generate_dummy_question_suggestions', + 'skill_id': None, + 'num_dummy_question_suggestions_generate': None + }, csrf_token=csrf_token) + + self.logout() + + def test_without_num_dummy_question_suggestions_generate_action_is_not_performed( # pylint: disable=line-too-long + self + ) -> None: + self.login(self.CURRICULUM_ADMIN_EMAIL, is_super_admin=True) + csrf_token = self.get_new_csrf_token() + + assert_raises_regexp_context_manager = self.assertRaisesRegex( + Exception, + 'The \'num_dummy_question_suggestions_generate\' must be provided' + ' when the action is _generate_dummy_question_suggestions.' + ) + with assert_raises_regexp_context_manager, self.prod_mode_swap: + self.post_json( + '/adminhandler', { + 'action': 'generate_dummy_question_suggestions', + 'skill_id': 'N8daS2n2aoQr', + 'num_dummy_question_suggestions_generate': None + }, csrf_token=csrf_token) + + self.logout() + def test_without_data_action_upload_topic_similarities_is_not_performed( self ) -> None: @@ -1360,6 +1403,100 @@ def test_cannot_generate_dummy_explorations_in_prod_mode(self) -> None: self.logout() +class GenerateDummyQuestionSuggestionsTest(test_utils.GenericTestBase): + """Test the conditions for generation of dummy question suggestions.""" + + def setUp(self) -> None: + super().setUp() + self.signup( + self.QUESTION_REVIEWER_EMAIL, self.QUESTION_REVIEWER_USERNAME) + self.signup( + self.QUESTION_ADMIN_EMAIL, + self.QUESTION_ADMIN_USERNAME, + is_super_admin=True) + self.question_reviewer_id = self.get_user_id_from_email( + self.QUESTION_REVIEWER_EMAIL) + self.add_user_role( + self.QUESTION_ADMIN_USERNAME, + feconf.ROLE_ID_QUESTION_ADMIN) + + def test_generate_dummy_question_suggestions_(self) -> None: + self.login(self.QUESTION_ADMIN_EMAIL, is_super_admin=True) + csrf_token = self.get_new_csrf_token() + self.post_json( + '/contributionrightshandler/submit_question', { + 'username': 'questionExpert' + }, csrf_token=csrf_token) + + self.post_json( + '/adminhandler', { + 'action': 'generate_dummy_question_suggestions', + 'skill_id': 'N8daS2n2aoQr', + 'num_dummy_question_suggestions_generate': 12 + }, csrf_token=csrf_token) + + generated_question_suggestions = suggestion_services.get_submitted_suggestions( # pylint: disable=line-too-long + self.get_user_id_from_email( + self.QUESTION_ADMIN_EMAIL), + feconf.SUGGESTION_TYPE_ADD_QUESTION) + self.assertEqual(len(generated_question_suggestions), 12) + self.logout() + + def test_cannot_generate_dummy_question_suggestions_in_prod_mode_(# pylint: disable=line-too-long + self) -> None: + self.login(self.QUESTION_ADMIN_EMAIL, is_super_admin=True) + csrf_token = self.get_new_csrf_token() + + prod_mode_swap = self.swap(constants, 'DEV_MODE', False) + assert_raises_regexp_context_manager = self.assertRaisesRegex( + Exception, 'Cannot generate dummy question suggestion in production.') + + self.post_json( + '/contributionrightshandler/submit_question', { + 'username': 'questionExpert' + }, csrf_token=csrf_token) + + with assert_raises_regexp_context_manager, prod_mode_swap: + self.post_json( + '/adminhandler', { + 'action': 'generate_dummy_question_suggestions', + 'skill_id': 'N8daS2n2aoQr', + 'num_dummy_question_suggestions_generate': 12 + }, csrf_token=csrf_token) + + generated_question_suggestions = suggestion_services.get_submitted_suggestions( # pylint: disable=line-too-long + self.get_user_id_from_email( + self.QUESTION_ADMIN_EMAIL), + feconf.SUGGESTION_TYPE_ADD_QUESTION) + self.assertNotEqual(len(generated_question_suggestions), 12) + self.logout() + + def test_raises_error_if_not_question_admin_or_question_submitter_(# pylint: disable=line-too-long + self) -> None: + self.login( + self.QUESTION_REVIEWER_EMAIL, is_super_admin=True) + csrf_token = self.get_new_csrf_token() + + assert_raises_regexp = self.assertRaisesRegex( + Exception, 'User \'question\' must be a question submitter or question admin' + ' in order to generate question suggestions.') + + with assert_raises_regexp: + self.post_json( + '/adminhandler', { + 'action': 'generate_dummy_question_suggestions', + 'skill_id': 'N8daS2n2aoQr', + 'num_dummy_question_suggestions_generate': 12 + }, csrf_token=csrf_token) + + generated_question_suggestions = suggestion_services.get_submitted_suggestions( # pylint: disable=line-too-long + self.get_user_id_from_email( + self.QUESTION_ADMIN_EMAIL), + feconf.SUGGESTION_TYPE_ADD_QUESTION) + self.assertNotEqual(len(generated_question_suggestions), 12) + self.logout() + + class GenerateDummyTranslationOpportunitiesTest(test_utils.GenericTestBase): """Checks the conditions for generation of dummy translation opportunities.""" diff --git a/core/controllers/contributor_dashboard_admin_test.py b/core/controllers/contributor_dashboard_admin_test.py index aaab842979c1..55b835da6999 100644 --- a/core/controllers/contributor_dashboard_admin_test.py +++ b/core/controllers/contributor_dashboard_admin_test.py @@ -44,16 +44,15 @@ class ContributionRightsHandlerTest(test_utils.GenericTestBase): """ TRANSLATION_REVIEWER_EMAIL: Final = 'translationreviewer@example.com' - QUESTION_REVIEWER_EMAIL: Final = 'questionreviewer@example.com' TRANSLATION_ADMIN_EMAIL: Final = 'translationadmin@example.com' - QUESTION_ADMIN_EMAIL: Final = 'questionadmin@example.com' def setUp(self) -> None: super().setUp() - self.signup(self.QUESTION_REVIEWER_EMAIL, 'question') + self.signup( + self.QUESTION_REVIEWER_EMAIL, self.QUESTION_REVIEWER_USERNAME) self.signup(self.TRANSLATION_REVIEWER_EMAIL, 'translator') self.signup(self.TRANSLATION_ADMIN_EMAIL, 'translationExpert') - self.signup(self.QUESTION_ADMIN_EMAIL, 'questionExpert') + self.signup(self.QUESTION_ADMIN_EMAIL, self.QUESTION_ADMIN_USERNAME) self.translation_reviewer_id = self.get_user_id_from_email( self.TRANSLATION_REVIEWER_EMAIL) @@ -357,16 +356,16 @@ class ContributorUsersListHandlerTest(test_utils.GenericTestBase): """Tests ContributorUsersListHandler.""" TRANSLATION_REVIEWER_EMAIL: Final = 'translationreviewer@example.com' - QUESTION_REVIEWER_EMAIL: Final = 'questionreviewer@example.com' TRANSLATION_ADMIN_EMAIL: Final = 'translationadmin@example.com' - QUESTION_ADMIN_EMAIL: Final = 'questionadmin@example.com' def setUp(self) -> None: super().setUp() self.signup(self.TRANSLATION_REVIEWER_EMAIL, 'translator') - self.signup(self.QUESTION_REVIEWER_EMAIL, 'question') + self.signup( + self.QUESTION_REVIEWER_EMAIL, + self.QUESTION_REVIEWER_USERNAME) self.signup(self.TRANSLATION_ADMIN_EMAIL, 'translationAdmen') - self.signup(self.QUESTION_ADMIN_EMAIL, 'questionAdmen') + self.signup(self.QUESTION_ADMIN_EMAIL, self.QUESTION_ADMIN_USERNAME) self.translation_reviewer_id = self.get_user_id_from_email( self.TRANSLATION_REVIEWER_EMAIL) diff --git a/core/controllers/cron_test.py b/core/controllers/cron_test.py index 3483bd699545..1620197a04ab 100644 --- a/core/controllers/cron_test.py +++ b/core/controllers/cron_test.py @@ -275,7 +275,7 @@ def _create_translation_suggestion( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': self.language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': self.default_translation_html, 'data_format': 'html' } @@ -477,7 +477,7 @@ def _create_translation_suggestion_for_en_language( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': self.language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': self.default_translation_html, 'data_format': 'html' } @@ -498,7 +498,7 @@ def _create_translation_suggestion_for_hi_language( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': 'hi', - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': self.default_translation_html, 'data_format': 'html' } @@ -694,7 +694,7 @@ def _create_translation_suggestion_with_language_code( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': '

This is the translated content.

', 'data_format': 'html' } diff --git a/core/domain/email_manager_test.py b/core/domain/email_manager_test.py index 86bbfb763685..242c4a14cdec 100644 --- a/core/domain/email_manager_test.py +++ b/core/domain/email_manager_test.py @@ -2862,7 +2862,7 @@ def _create_translation_suggestion_in_lang_with_html_and_datetime( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': translation_html, 'data_format': 'html' } @@ -2886,7 +2886,7 @@ def _create_question_suggestion_with_question_html_and_datetime( submission datetime. """ with self.swap( - feconf, 'DEFAULT_INIT_STATE_CONTENT_STR', question_html): + feconf, 'DEFAULT_STATE_CONTENT_STR', question_html): content_id_generator = translation_domain.ContentIdGenerator() add_question_change_dict: Dict[ str, Union[str, float, question_domain.QuestionDict] @@ -4581,7 +4581,7 @@ def _create_translation_suggestion_in_lang_with_html_and_datetime( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': translation_html, 'data_format': 'html' } @@ -4605,7 +4605,7 @@ def _create_question_suggestion_with_question_html_and_datetime( submission datetime. """ with self.swap( - feconf, 'DEFAULT_INIT_STATE_CONTENT_STR', question_html): + feconf, 'DEFAULT_STATE_CONTENT_STR', question_html): content_id_generator = translation_domain.ContentIdGenerator() add_question_change_dict: Dict[ str, Union[str, float, question_domain.QuestionDict] @@ -5545,7 +5545,7 @@ def _create_translation_suggestion_in_lang_with_html_and_datetime( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': translation_html, 'data_format': 'html' } @@ -5704,7 +5704,7 @@ def _create_translation_suggestion_with_language_code( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': '

This is the translated content.

', 'data_format': 'html' } @@ -7065,7 +7065,6 @@ class CDUserEmailTest(test_utils.EmailTestBase): """Test for assignment and removal of contribution reviewers.""" TRANSLATION_REVIEWER_EMAIL: Final = 'translationreviewer@example.com' - QUESTION_REVIEWER_EMAIL: Final = 'questionreviewer@example.com' QUESTION_SUBMITTER_EMAIL: Final = 'questionsubmitter@example.com' def setUp(self) -> None: diff --git a/core/domain/exp_domain_test.py b/core/domain/exp_domain_test.py index 0eb3dc46f93b..6409c9e3fe0c 100644 --- a/core/domain/exp_domain_test.py +++ b/core/domain/exp_domain_test.py @@ -12653,7 +12653,7 @@ def _get_default_state_dict( 'objective': feconf.DEFAULT_EXPLORATION_OBJECTIVE, 'states': { feconf.DEFAULT_INIT_STATE_NAME: _get_default_state_dict( - feconf.DEFAULT_INIT_STATE_CONTENT_STR, + feconf.DEFAULT_STATE_CONTENT_STR, feconf.DEFAULT_INIT_STATE_NAME, True, content_id_generator), second_state_name: _get_default_state_dict( '', second_state_name, False, content_id_generator), diff --git a/core/domain/question_services_test.py b/core/domain/question_services_test.py index 31aacab9cc65..2efb0c98466b 100644 --- a/core/domain/question_services_test.py +++ b/core/domain/question_services_test.py @@ -491,7 +491,7 @@ def test_get_question_summaries_by_ids(self) -> None: self.assertEqual(question_summaries[0].id, self.question_id) self.assertEqual( question_summaries[0].question_content, - feconf.DEFAULT_INIT_STATE_CONTENT_STR) + feconf.DEFAULT_STATE_CONTENT_STR) self.assertIsNone(question_summaries[1]) def test_delete_question(self) -> None: @@ -768,7 +768,7 @@ def test_compute_summary_of_question(self) -> None: self.assertEqual(question_summary.id, self.question_id) self.assertEqual( question_summary.question_content, - feconf.DEFAULT_INIT_STATE_CONTENT_STR) + feconf.DEFAULT_STATE_CONTENT_STR) def test_raises_error_while_computing_summary_if_interaction_id_is_none( self diff --git a/core/domain/state_domain.py b/core/domain/state_domain.py index e50ef8ab0734..d489129b12c9 100644 --- a/core/domain/state_domain.py +++ b/core/domain/state_domain.py @@ -4320,8 +4320,7 @@ def create_default_state( Returns: State. The corresponding State domain object. """ - content_html = ( - feconf.DEFAULT_INIT_STATE_CONTENT_STR if is_initial_state else '') + content_html = feconf.DEFAULT_STATE_CONTENT_STR recorded_voiceovers = RecordedVoiceovers({}) recorded_voiceovers.add_content_id_for_voiceover( diff --git a/core/domain/suggestion_services_test.py b/core/domain/suggestion_services_test.py index 1d747757428c..04c0cf07da96 100644 --- a/core/domain/suggestion_services_test.py +++ b/core/domain/suggestion_services_test.py @@ -4687,7 +4687,7 @@ def _create_translation_suggestion_with_translation_html( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': self.language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': translation_html, 'data_format': 'html' } @@ -4707,7 +4707,7 @@ def _create_question_suggestion_with_question_html_content( question in the question suggestion. """ with self.swap( - feconf, 'DEFAULT_INIT_STATE_CONTENT_STR', question_html_content): + feconf, 'DEFAULT_STATE_CONTENT_STR', question_html_content): content_id_generator = translation_domain.ContentIdGenerator() add_question_change_dict: Dict[ str, Union[str, float, question_domain.QuestionDict] @@ -5410,7 +5410,7 @@ def _create_translation_suggestion_with_language_code_and_author( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': '

This is the translated content.

', 'data_format': 'html' } @@ -6064,7 +6064,7 @@ def _create_translation_suggestion_with_language_code( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': '

This is the translated content.

', 'data_format': 'html' } @@ -6638,7 +6638,7 @@ def _create_translation_suggestion( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': self.language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': '

This is the translated content.

', 'data_format': 'html' } @@ -6969,7 +6969,7 @@ def _create_translation_suggestion_with_language_code( 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': 'content_0', 'language_code': language_code, - 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, + 'content_html': feconf.DEFAULT_STATE_CONTENT_STR, 'translation_html': '

This is the translated content.

', 'data_format': 'html' } diff --git a/core/domain/user_services_test.py b/core/domain/user_services_test.py index 052dc900bc8b..f08a6a06446c 100644 --- a/core/domain/user_services_test.py +++ b/core/domain/user_services_test.py @@ -3625,10 +3625,6 @@ class UserContributionReviewRightsTests(test_utils.GenericTestBase): TRANSLATOR_EMAIL: Final = 'translator@community.org' TRANSLATOR_USERNAME: Final = 'translator' - - QUESTION_REVIEWER_EMAIL: Final = 'question@community.org' - QUESTION_REVIEWER_USERNAME: Final = 'questionreviewer' - QUESTION_SUBMITTER_EMAIL: Final = 'submitter@community.org' QUESTION_SUBMITTER_USERNAME: Final = 'questionsubmitter' diff --git a/core/feconf.py b/core/feconf.py index 55609e00d852..c98ab927ab1a 100644 --- a/core/feconf.py +++ b/core/feconf.py @@ -421,7 +421,7 @@ class ClassifierDict(TypedDict): # customization argument choices. INVALID_CONTENT_ID = 'invalid_content_id' # The default content text for the initial state of an exploration. -DEFAULT_INIT_STATE_CONTENT_STR = '' +DEFAULT_STATE_CONTENT_STR = '' # Whether new explorations should have automatic text-to-speech enabled # by default. diff --git a/core/templates/domain/admin/admin-backend-api.service.spec.ts b/core/templates/domain/admin/admin-backend-api.service.spec.ts index 273f904acf6e..c558321fabde 100644 --- a/core/templates/domain/admin/admin-backend-api.service.spec.ts +++ b/core/templates/domain/admin/admin-backend-api.service.spec.ts @@ -30,6 +30,7 @@ import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; import {PlatformParameterFilterType} from 'domain/platform-parameter/platform-parameter-filter.model'; import {PlatformParameter} from 'domain/platform-parameter/platform-parameter.model'; import {CsrfTokenService} from 'services/csrf-token.service'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; import {AdminPageConstants} from 'pages/admin-page/admin-page.constants'; describe('Admin backend api service', () => { @@ -96,6 +97,28 @@ describe('Admin backend api service', () => { default_value: '', }, ], + skill_list: [ + { + id: 'ByBjUvYOITCJ', + description: 'Dummy Skill 3', + language_code: 'en', + version: 1, + misconception_count: 0, + worked_examples_count: 0, + skill_model_created_on: 1711790151279.081, + skill_model_last_updated: 1711790151279.083, + }, + { + id: 'TybOaLMmNeO1', + description: 'Dummy Skill 1', + language_code: 'en', + version: 1, + misconception_count: 0, + worked_examples_count: 0, + skill_model_created_on: 1711790151229.939, + skill_model_last_updated: 1711790151229.944, + }, + ], }; let adminDataObject: AdminPageData; @@ -123,6 +146,9 @@ describe('Admin backend api service', () => { platformParameters: adminBackendResponse.platform_params_dicts.map(dict => PlatformParameter.createFromBackendDict(dict) ), + skillList: adminBackendResponse.skill_list.map(dict => + SkillSummary.createFromBackendDict(dict) + ), }; spyOn(csrfService, 'getTokenAsync').and.callFake(async () => { @@ -1345,6 +1371,62 @@ describe('Admin backend api service', () => { expect(failHandler).not.toHaveBeenCalled(); })); + it('should generate dummy suggestion questions', fakeAsync(() => { + let action = 'generate_dummy_question_suggestions'; + let skillId = 'dc3k2ldd'; + let numberOfQuestions = 2; + let payload = { + action: action, + skill_id: skillId, + num_dummy_question_suggestions_generate: numberOfQuestions, + }; + + abas + .generateDummySuggestionQuestionsAsync(skillId, numberOfQuestions) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/adminhandler'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(payload); + req.flush(200); + flushMicrotasks(); + + expect(successHandler).toHaveBeenCalled(); + expect(failHandler).not.toHaveBeenCalled(); + })); + + it('should handle generate dummy suggestion questions request failure', fakeAsync(() => { + let action = 'generate_dummy_question_suggestions'; + let skillId = 'dc3k2ldd'; + let numberOfQuestions = 2; + let payload = { + action: action, + skill_id: skillId, + num_dummy_question_suggestions_generate: numberOfQuestions, + }; + + abas + .generateDummySuggestionQuestionsAsync(skillId, numberOfQuestions) + .then(successHandler, failHandler); + + let req = httpTestingController.expectOne('/adminhandler'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual(payload); + req.flush( + { + error: 'Failed to get data.', + }, + { + status: 500, + statusText: 'Internal Server Error', + } + ); + flushMicrotasks(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(failHandler).toHaveBeenCalledWith('Failed to get data.'); + })); + it('should handle generate dummy blog request failure', fakeAsync(() => { let action = 'generate_dummy_blog_post'; let blogPostTitle = 'Education'; diff --git a/core/templates/domain/admin/admin-backend-api.service.ts b/core/templates/domain/admin/admin-backend-api.service.ts index 3085828535c1..2580da51acdc 100644 --- a/core/templates/domain/admin/admin-backend-api.service.ts +++ b/core/templates/domain/admin/admin-backend-api.service.ts @@ -29,6 +29,10 @@ import { CreatorTopicSummary, CreatorTopicSummaryBackendDict, } from 'domain/topic/creator-topic-summary.model'; +import { + SkillSummary, + SkillSummaryBackendDict, +} from 'domain/skill/skill-summary.model'; import {UrlInterpolationService} from 'domain/utilities/url-interpolation.service'; export interface UserRolesBackendResponse { @@ -82,6 +86,7 @@ export interface AdminPageDataBackendDict { human_readable_roles: HumanReadableRolesBackendResponse; topic_summaries: CreatorTopicSummaryBackendDict[]; platform_params_dicts: PlatformParameterBackendDict[]; + skill_list: SkillSummaryBackendDict[]; } export interface AdminPageData { @@ -94,6 +99,7 @@ export interface AdminPageData { humanReadableRoles: HumanReadableRolesBackendResponse; topicSummaries: CreatorTopicSummary[]; platformParameters: PlatformParameter[]; + skillList: SkillSummary[]; } export interface ExplorationInteractionIdsBackendResponse { @@ -130,6 +136,9 @@ export class AdminBackendApiService { platformParameters: response.platform_params_dicts.map(dict => PlatformParameter.createFromBackendDict(dict) ), + skillList: response.skill_list.map(dict => + SkillSummary.createFromBackendDict(dict) + ), }); }, errorResponse => { @@ -632,6 +641,17 @@ export class AdminBackendApiService { }); } + async generateDummySuggestionQuestionsAsync( + skillId: string, + numberOfQuestions: number + ): Promise { + return this._postRequestAsync(AdminPageConstants.ADMIN_HANDLER_URL, { + action: 'generate_dummy_question_suggestions', + skill_id: skillId, + num_dummy_question_suggestions_generate: numberOfQuestions, + }); + } + async retrieveExplorationInteractionIdsAsync( expId: string ): Promise { diff --git a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.html b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.html index a914365b9b1a..e185855a86b4 100644 --- a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.html +++ b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.html @@ -165,6 +165,36 @@

Generate a dummy math classroom (only curriculum admins)

+ +
+

Generate dummy suggestion questions (only question admins)

+ + + + + + + +

Note: To generate question, first generate skills and assign yourself + question admin role and permission to suggest questions through contributor admin dashboard. +

+ + +
+
+
+

Generate dummy blog post.

diff --git a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts index cf4790790c19..bb045f121207 100644 --- a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts +++ b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.spec.ts @@ -47,6 +47,12 @@ describe('Admin dev mode activities tab', () => { demoExplorationIds: ['expId'], demoExplorations: [['0', 'welcome.yaml']], demoCollections: [['collectionId']], + skillList: [ + { + id: 'Fg6LbD9h2Eg4', + description: 'Skill1', + }, + ], } as AdminPageData; let mockConfirmResult: (val: boolean) => void; @@ -611,6 +617,127 @@ describe('Admin dev mode activities tab', () => { })); }); + describe('.generateDummyExploration', () => { + it( + 'should not generate dummy exploration if publish count is greater' + + 'than generate count', + () => { + let adminBackendSpy = spyOn( + adminBackendApiService, + 'generateDummyExplorationsAsync' + ); + + component.numDummyExpsToPublish = 2; + component.numDummyExpsToGenerate = 1; + + spyOn(component.setStatusMessage, 'emit'); + + component.generateDummyExplorations(); + + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Publish count should be less than or equal to generate count' + ); + expect(adminBackendSpy).not.toHaveBeenCalled(); + } + ); + + it('should generate dummy explorations', async(() => { + component.numDummyExpsToPublish = 1; + component.numDummyExpsToGenerate = 2; + + spyOn( + adminBackendApiService, + 'generateDummyExplorationsAsync' + ).and.returnValue(Promise.resolve()); + spyOn(component.setStatusMessage, 'emit'); + + component.generateDummyExplorations(); + + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); + + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Dummy explorations generated successfully.' + ); + }); + })); + + it('should show error message when dummy explorations are not generated', async(() => { + component.numDummyExpsToPublish = 2; + component.numDummyExpsToGenerate = 2; + + spyOn( + adminBackendApiService, + 'generateDummyExplorationsAsync' + ).and.returnValue(Promise.reject('Dummy explorations not generated.')); + spyOn(component.setStatusMessage, 'emit'); + + component.generateDummyExplorations(); + + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); + + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: Dummy explorations not generated.' + ); + }); + })); + }); + + describe('.generateDummySuggestionQuestions', () => { + it('should generate dummy suggestion questions', async () => { + spyOn( + adminBackendApiService, + 'generateDummySuggestionQuestionsAsync' + ).and.returnValue(Promise.resolve()); + spyOn(component.setStatusMessage, 'emit'); + + component.numDummySuggestionQuesToGenerate = 2; + + component.generateDummySuggestionQuestions('0'); + + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Dummy suggestion questions generated successfully.' + ); + }); + }); + + it( + 'should show error message when dummy suggestion questions' + + 'are not generated', + async () => { + spyOn( + adminBackendApiService, + 'generateDummySuggestionQuestionsAsync' + ).and.returnValue( + Promise.reject('Dummy suggestion questions not generated.') + ); + spyOn(component.setStatusMessage, 'emit'); + + component.numDummySuggestionQuesToGenerate = 2; + + component.generateDummySuggestionQuestions('0'); + + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Processing...' + ); + fixture.whenStable().then(() => { + expect(component.setStatusMessage.emit).toHaveBeenCalledWith( + 'Server error: Dummy suggestion questions not generated.' + ); + }); + } + ); + }); + describe('.reloadCollection', () => { it('should not reload collection if a task is already running', () => { let adminBackendSpy = spyOn( diff --git a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts index 51114947250a..0a4589653ecb 100644 --- a/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts +++ b/core/templates/pages/admin-page/activities-tab/admin-dev-mode-activities-tab.component.ts @@ -22,6 +22,7 @@ import {Component, Output, OnInit, EventEmitter} from '@angular/core'; import {AdminBackendApiService} from 'domain/admin/admin-backend-api.service'; import {AdminDataService} from 'pages/admin-page/services/admin-data.service'; import {AdminTaskManagerService} from 'pages/admin-page/services/admin-task-manager.service'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; import {WindowRef} from 'services/contextual/window-ref.service'; @Component({ @@ -34,6 +35,9 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { demoExplorationIds: string[] = []; numDummyExpsToPublish: number = 0; numDummyExpsToGenerate: number = 0; + numDummySuggestionQuesToGenerate: number = 0; + skillList: SkillSummary[] = []; + selectedOption: string = ''; numDummyTranslationOpportunitiesToGenerate: number = 0; DEMO_COLLECTIONS: string[][] = [[]]; DEMO_EXPLORATIONS: string[][] = [[]]; @@ -154,6 +158,7 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { ) .then( () => { + this.getDataAsync(); this.setStatusMessage.emit( 'Dummy explorations generated successfully.' ); @@ -192,6 +197,7 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminBackendApiService.generateDummyNewStructuresDataAsync().then( () => { + this.getDataAsync(); this.setStatusMessage.emit( 'Dummy new structures data generated successfully.' ); @@ -209,6 +215,7 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminBackendApiService.generateDummyNewSkillDataAsync().then( () => { + this.getDataAsync(); this.setStatusMessage.emit( 'Dummy new skill and questions generated successfully.' ); @@ -220,6 +227,30 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminTaskManagerService.finishTask(); } + generateDummySuggestionQuestions(selectedOption: string): void { + // Generate dummy suggestion question for the selected skill. + const selectedIndex = Number(selectedOption); + let selectedSkill = this.skillList[selectedIndex]; + this.adminTaskManagerService.startTask(); + this.setStatusMessage.emit('Processing...'); + this.adminBackendApiService + .generateDummySuggestionQuestionsAsync( + selectedSkill.id, + this.numDummySuggestionQuesToGenerate + ) + .then( + () => { + this.setStatusMessage.emit( + 'Dummy suggestion questions generated successfully.' + ); + }, + errorResponse => { + this.setStatusMessage.emit('Server error: ' + errorResponse); + } + ); + this.adminTaskManagerService.finishTask(); + } + generateNewBlogPost(blogPostTitle: string): void { if (!blogPostTitle) { this.setStatusMessage.emit('Internal error: blogPostTitle is empty'); @@ -243,6 +274,7 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.adminBackendApiService.generateDummyClassroomDataAsync().then( () => { + this.getDataAsync(); this.setStatusMessage.emit( 'Dummy new classroom generated successfully.' ); @@ -288,6 +320,7 @@ export class AdminDevModeActivitiesTabComponent implements OnInit { this.DEMO_COLLECTIONS = adminDataObject.demoCollections; this.demoExplorationIds = adminDataObject.demoExplorationIds; this.reloadingAllExplorationPossible = true; + this.skillList = adminDataObject.skillList; } ngOnInit(): void { diff --git a/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts b/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts index 9d10312c3875..50c18b5d117f 100644 --- a/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts +++ b/core/templates/pages/admin-page/roles-tab/admin-roles-tab.component.spec.ts @@ -93,6 +93,7 @@ describe('Admin roles tab component ', function () { }, topicSummaries: [sampleTopicSummary], platformParameters: [], + skillList: [], }; beforeEach(() => { diff --git a/core/templates/pages/admin-page/services/admin-data.service.spec.ts b/core/templates/pages/admin-page/services/admin-data.service.spec.ts index 3b84d58e2c4b..cea93a37901f 100644 --- a/core/templates/pages/admin-page/services/admin-data.service.spec.ts +++ b/core/templates/pages/admin-page/services/admin-data.service.spec.ts @@ -30,6 +30,7 @@ import { AdminPageDataBackendDict, } from 'domain/admin/admin-backend-api.service'; import {CreatorTopicSummary} from 'domain/topic/creator-topic-summary.model'; +import {SkillSummary} from 'domain/skill/skill-summary.model'; describe('Admin Data Service', () => { let adminDataService: AdminDataService; @@ -94,6 +95,7 @@ describe('Admin Data Service', () => { default_value: '', }, ], + skill_list: [], }; let adminDataResponse: AdminPageData; @@ -118,6 +120,9 @@ describe('Admin Data Service', () => { platformParameters: sampleAdminData.platform_params_dicts.map(dict => PlatformParameter.createFromBackendDict(dict) ), + skillList: sampleAdminData.skill_list.map(dict => + SkillSummary.createFromBackendDict(dict) + ), }; }); diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py index e1b09f9cc3dd..71c2cec1c226 100644 --- a/core/tests/test_utils.py +++ b/core/tests/test_utils.py @@ -2197,6 +2197,10 @@ class GenericTestBase(AppEngineTestBase): OWNER_USERNAME: Final = 'owner' EDITOR_EMAIL: Final = 'editor@example.com' EDITOR_USERNAME: Final = 'editor' + QUESTION_ADMIN_EMAIL: Final = 'questionExpert@app.com' + QUESTION_ADMIN_USERNAME: Final = 'questionExpert' + QUESTION_REVIEWER_EMAIL: Final = 'questionreviewer@example.com' + QUESTION_REVIEWER_USERNAME: Final = 'question' TOPIC_MANAGER_EMAIL: Final = 'topicmanager@example.com' TOPIC_MANAGER_USERNAME: Final = 'topicmanager' VOICE_ARTIST_EMAIL: Final = 'voiceartist@example.com'