diff --git a/.circleci/config.yml b/.circleci/config.yml index acd900c31f4a..badcac80abc8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,7 +80,7 @@ jobs: name: Generate frontend coverage report command: | sudo pip install codecov - codecov --file ../karma_coverage_reports/coverage-final.json + codecov --file ../karma_coverage_reports/lcov.info when: on_success backend_tests: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c38da8447193..cad7e4397bc9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -251,19 +251,19 @@ # New structures team. # Question project. /core/controllers/practice_sessions*.py @vinitamurthi -/core/controllers/question*.py @aks681 -/core/controllers/review_tests*.py @sophiewu6 -/core/domain/question*.py @aks681 -/core/storage/question/ @aks681 -/core/templates/dev/head/components/entity-creation-services/question-creation.service.ts @aks681 +/core/controllers/question*.py @aks681 @vinitamurthi +/core/controllers/review_tests*.py @sophiewu6 @vinitamurthi +/core/domain/question*.py @aks681 @vinitamurthi +/core/storage/question/ @aks681 @vinitamurthi +/core/templates/dev/head/components/entity-creation-services/question-creation.service.ts @aks681 @vinitamurthi /core/templates/dev/head/components/score-ring/ @vinitamurthi -/core/templates/dev/head/domain/question/ @aks681 +/core/templates/dev/head/domain/question/ @aks681 @vinitamurthi /core/templates/dev/head/pages/practice-session-page/ @vinitamurthi -/core/templates/dev/head/pages/review-test-page/ @sophiewu6 -/core/templates/dev/head/services/QuestionsListService.ts @sophiewu6 -/core/templates/dev/head/components/question-directives/question-editor/ @aks681 -/core/templates/dev/head/components/question-directives/questions-list/ @aks681 -/core/templates/dev/head/components/question-directives/modal-templates/ @aks681 +/core/templates/dev/head/pages/review-test-page/ @sophiewu6 @vinitamurthi +/core/templates/dev/head/services/QuestionsListService.ts @sophiewu6 @vinitamurthi +/core/templates/dev/head/components/question-directives/question-editor/ @aks681 @vinitamurthi +/core/templates/dev/head/components/question-directives/questions-list/ @aks681 @vinitamurthi +/core/templates/dev/head/components/question-directives/modal-templates/ @aks681 @vinitamurthi /core/templates/dev/head/components/question-directives/question-player/ @vinitamurthi # Skill project. diff --git a/core/controllers/question_editor.py b/core/controllers/question_editor.py index ca5823aab670..69a554106e6c 100644 --- a/core/controllers/question_editor.py +++ b/core/controllers/question_editor.py @@ -30,9 +30,14 @@ class QuestionCreationHandler(base.BaseHandler): """A handler that creates the question model given a question dict.""" @acl_decorators.can_manage_question_skill_status - def post(self, comma_separated_skill_ids): + def post(self): """Handles POST requests.""" - skill_ids = comma_separated_skill_ids.split(',') + skill_ids = self.payload.get('skill_ids') + + if not skill_ids: + raise self.InvalidInputException( + 'skill_ids parameter isn\'t present in the payload') + if len(skill_ids) > constants.MAX_SKILLS_PER_QUESTION: raise self.InvalidInputException( 'More than %d QuestionSkillLinks for one question ' @@ -40,12 +45,12 @@ def post(self, comma_separated_skill_ids): try: for skill_id in skill_ids: skill_domain.Skill.require_valid_skill_id(skill_id) - except Exception: - raise self.InvalidInputException + except Exception as e: + raise self.InvalidInputException('Skill ID(s) aren\'t valid: ', e) try: skill_services.get_multi_skills(skill_ids) - except Exception, e: + except Exception as e: raise self.PageNotFoundException(e) question_dict = self.payload.get('question_dict') @@ -54,24 +59,49 @@ def post(self, comma_separated_skill_ids): ('question_state_data' not in question_dict) or ('language_code' not in question_dict) or (question_dict['version'] != 1)): - raise self.InvalidInputException + raise self.InvalidInputException( + 'Question Data should contain id, state data, language code, ' + + 'and its version should be set as 1') question_dict['question_state_data_schema_version'] = ( feconf.CURRENT_STATE_SCHEMA_VERSION) question_dict['id'] = question_services.get_new_question_id() question_dict['linked_skill_ids'] = skill_ids + try: question = question_domain.Question.from_dict(question_dict) - except Exception: - raise self.InvalidInputException + except Exception as e: + raise self.InvalidInputException( + 'Question structure is invalid:', e) + + skill_difficulties = self.payload.get('skill_difficulties') + + if not skill_difficulties: + raise self.InvalidInputException( + 'skill_difficulties not present in the payload') + if len(skill_ids) != len(skill_difficulties): + raise self.InvalidInputException( + 'Skill difficulties don\'t match up with skill IDs') + + try: + skill_difficulties = [ + float(difficulty) for difficulty in skill_difficulties] + except (ValueError, TypeError): + raise self.InvalidInputException( + 'Skill difficulties must be a float value') + + if any(( + difficulty < 0 or difficulty > 1) + for difficulty in skill_difficulties): + raise self.InvalidInputException( + 'Skill difficulties must be between 0 and 1') + question_services.add_question(self.user_id, question) - # TODO(vinitamurthi): Replace DEFAULT_SKILL_DIFFICULTY - # with a value passed from the frontend. question_services.link_multiple_skills_for_question( self.user_id, question.id, skill_ids, - [constants.DEFAULT_SKILL_DIFFICULTY] * len(skill_ids)) + skill_difficulties) self.values.update({ 'question_id': question.id diff --git a/core/controllers/question_editor_test.py b/core/controllers/question_editor_test.py index 56f6a6a8a6ff..8757917c733b 100644 --- a/core/controllers/question_editor_test.py +++ b/core/controllers/question_editor_test.py @@ -69,8 +69,9 @@ def test_post_with_non_admin_or_topic_manager_email_disallows_access(self): self.login(self.NEW_USER_EMAIL) csrf_token = self.get_new_csrf_token() self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), - {}, csrf_token=csrf_token, expected_status_int=401) + feconf.NEW_QUESTION_URL, { + 'skill_ids': [self.skill_id] + }, csrf_token=csrf_token, expected_status_int=401) self.logout() def test_post_with_editor_email_does_not_allow_question_creation(self): @@ -79,8 +80,9 @@ def test_post_with_editor_email_does_not_allow_question_creation(self): question_dict = self.question.to_dict() question_dict['id'] = None self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), { - 'question_dict': question_dict + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id] }, csrf_token=csrf_token, expected_status_int=401) self.logout() @@ -89,17 +91,27 @@ def test_post_with_incorrect_skill_id_returns_404(self): csrf_token = self.get_new_csrf_token() incorrect_skill_id = 'abc123456789' self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, incorrect_skill_id), - {}, csrf_token=csrf_token, expected_status_int=404) + feconf.NEW_QUESTION_URL, { + 'skill_ids': [incorrect_skill_id] + }, csrf_token=csrf_token, expected_status_int=404) + self.logout() + + def test_post_with_no_skill_ids_returns_400(self): + self.login(self.ADMIN_EMAIL) + csrf_token = self.get_new_csrf_token() + self.post_json( + feconf.NEW_QUESTION_URL, {}, + csrf_token=csrf_token, expected_status_int=400) self.logout() def test_post_with_incorrect_list_of_skill_ids_returns_400(self): self.login(self.ADMIN_EMAIL) csrf_token = self.get_new_csrf_token() - incorrect_skill_id = [1, 2] + incorrect_skill_ids = [1, 2] self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, incorrect_skill_id), - {}, csrf_token=csrf_token, expected_status_int=400) + feconf.NEW_QUESTION_URL, { + 'skill_ids': incorrect_skill_ids + }, csrf_token=csrf_token, expected_status_int=400) self.logout() def test_post_with_incorrect_type_of_skill_ids_returns_400(self): @@ -107,8 +119,9 @@ def test_post_with_incorrect_type_of_skill_ids_returns_400(self): csrf_token = self.get_new_csrf_token() incorrect_skill_id = 1 self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, incorrect_skill_id), - {}, csrf_token=csrf_token, expected_status_int=400) + feconf.NEW_QUESTION_URL, { + 'skill_ids': [incorrect_skill_id], + }, csrf_token=csrf_token, expected_status_int=400) self.logout() def test_post_with_incorrect_question_id_returns_400(self): @@ -117,8 +130,9 @@ def test_post_with_incorrect_question_id_returns_400(self): question_dict = self.question.to_dict() question_dict['id'] = 'abc123456789' self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), { - 'question_dict': question_dict + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id] }, csrf_token=csrf_token, expected_status_int=400) self.logout() @@ -128,8 +142,60 @@ def test_post_with_incorrect_question_schema_returns_400(self): question_dict = self.question.to_dict() del question_dict['question_state_data']['content'] self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), { - 'question_dict': question_dict + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], + }, csrf_token=csrf_token, expected_status_int=400) + self.logout() + + def test_post_with_no_skill_difficulty_returns_400(self): + self.login(self.ADMIN_EMAIL) + csrf_token = self.get_new_csrf_token() + question_dict = self.question.to_dict() + question_dict['id'] = None + self.post_json( + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id] + }, csrf_token=csrf_token, expected_status_int=400) + self.logout() + + def test_post_with_wrong_skill_difficulty_length_returns_400(self): + self.login(self.ADMIN_EMAIL) + csrf_token = self.get_new_csrf_token() + question_dict = self.question.to_dict() + question_dict['id'] = None + self.post_json( + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], + 'skill_difficulties': [0.6, 0.8] + }, csrf_token=csrf_token, expected_status_int=400) + self.logout() + + def test_post_with_invalid_skill_difficulty_type_returns_400(self): + self.login(self.ADMIN_EMAIL) + csrf_token = self.get_new_csrf_token() + question_dict = self.question.to_dict() + question_dict['id'] = None + self.post_json( + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], + 'skill_difficulties': ['test'] + }, csrf_token=csrf_token, expected_status_int=400) + self.logout() + + def test_post_with_invalid_skill_difficulty_value_returns_400(self): + self.login(self.ADMIN_EMAIL) + csrf_token = self.get_new_csrf_token() + question_dict = self.question.to_dict() + question_dict['id'] = None + self.post_json( + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], + 'skill_difficulties': [2.0] }, csrf_token=csrf_token, expected_status_int=400) self.logout() @@ -139,8 +205,10 @@ def test_post_with_admin_email_allows_question_creation(self): question_dict = self.question.to_dict() question_dict['id'] = None self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), { - 'question_dict': question_dict + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], + 'skill_difficulties': [0.6] }, csrf_token=csrf_token, expected_status_int=200) all_models = question_models.QuestionModel.get_all() questions = [ @@ -156,8 +224,10 @@ def test_post_with_topic_manager_email_allows_question_creation(self): question_dict = self.question.to_dict() question_dict['id'] = None self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), { - 'question_dict': question_dict + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], + 'skill_difficulties': [0.6] }, csrf_token=csrf_token) all_models = question_models.QuestionModel.get_all() questions = [ @@ -174,8 +244,9 @@ def test_post_with_invalid_question_returns_400_status(self): question_dict['id'] = None question_dict['question_state_data'] = 'invalid_question_state_data' self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, self.skill_id), { - 'question_dict': question_dict + feconf.NEW_QUESTION_URL, { + 'question_dict': question_dict, + 'skill_ids': [self.skill_id], }, csrf_token=csrf_token, expected_status_int=400) self.logout() @@ -184,8 +255,9 @@ def test_post_with_too_many_skills_returns_400(self): csrf_token = self.get_new_csrf_token() skill_ids = [1, 2, 3, 4] self.post_json( - '%s/%s' % (feconf.NEW_QUESTION_URL, skill_ids), - {}, csrf_token=csrf_token, expected_status_int=400) + feconf.NEW_QUESTION_URL, { + 'skill_ids': skill_ids + }, csrf_token=csrf_token, expected_status_int=400) self.logout() diff --git a/core/domain/classifier_domain_test.py b/core/domain/classifier_domain_test.py index 264ea2416dcc..e9f482fc2824 100644 --- a/core/domain/classifier_domain_test.py +++ b/core/domain/classifier_domain_test.py @@ -217,33 +217,25 @@ def test_validation_training_data_with_invalid_answers_type(self): utils.ValidationError, 'Expected answers to be a list'): training_job.validate() - def test_validation(self): - """Tests to verify validate method of ClassifierTrainingJob domain.""" - - # Verify no errors are raised for correct data. + def test_validation_for_training_job_with_correct_data(self): training_job = self._get_training_job_from_dict(self.training_job_dict) training_job.validate() - # Verify validation error is raised when int is provided for job id - # instead of string. + def test_validation_with_invalid_job_id(self): self.training_job_dict['job_id'] = 1 training_job = self._get_training_job_from_dict(self.training_job_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Expected id to be a string'): training_job.validate() - # Verify validation error is raised when string is provided for - # exp_version instead of int. - self.training_job_dict['job_id'] = 'exp_id1.SOME_RANDOM_STRING' + def test_validation_with_invalid_exp_version(self): self.training_job_dict['exp_version'] = 'abc' training_job = self._get_training_job_from_dict(self.training_job_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Expected exp_version to be an int'): training_job.validate() - # Verify validation error is raised when string is provided for - # next_scheduled_check_time instead of datetime. - self.training_job_dict['exp_version'] = 1 + def test_validation_with_invalid_next_scheduled_check_time(self): self.training_job_dict['next_scheduled_check_time'] = 'abc' training_job = self._get_training_job_from_dict(self.training_job_dict) with self.assertRaisesRegexp( @@ -251,27 +243,21 @@ def test_validation(self): 'Expected next_scheduled_check_time to be datetime'): training_job.validate() - # Verify validation error is raised when invalid state_name is provided. - self.training_job_dict['next_scheduled_check_time'] = ( - datetime.datetime.strptime( - '2017-08-11 12:42:31', '%Y-%m-%d %H:%M:%S')) + def test_validation_with_invalid_state_name(self): self.training_job_dict['state_name'] = 'A string #' training_job = self._get_training_job_from_dict(self.training_job_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Invalid character # in the state name'): training_job.validate() - # Verify validation error is raised when invalid algorithm_id is - # provided. - self.training_job_dict['state_name'] = 'a state name' + def test_validation_with_invalid_algorithm_id(self): self.training_job_dict['algorithm_id'] = 'abc' training_job = self._get_training_job_from_dict(self.training_job_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Invalid algorithm id'): training_job.validate() - # Verify validation error is raised when dict is provided for list. - self.training_job_dict['algorithm_id'] = 'TextClassifier' + def test_validation_with_invalid_training_data(self): self.training_job_dict['training_data'] = {} training_job = self._get_training_job_from_dict(self.training_job_dict) with self.assertRaisesRegexp( @@ -281,6 +267,15 @@ def test_validation(self): class TrainingJobExplorationMappingDomainTests(test_utils.GenericTestBase): """Tests for the TrainingJobExplorationMapping domain.""" + def setUp(self): + super(TrainingJobExplorationMappingDomainTests, self).setUp() + + self.mapping_dict = { + 'exp_id': 'exp_id1', + 'exp_version': 2, + 'state_name': u'網站有中', + 'job_id': 'job_id1' + } def _get_mapping_from_dict(self, mapping_dict): """Returns the TrainingJobExplorationMapping object after receiving the @@ -307,52 +302,34 @@ def test_to_dict(self): expected_mapping_dict, observed_mapping.to_dict()) - def test_validation(self): - """Tests to verify validate method of TrainingJobExplorationMapping - domain. - """ - - # Verify no errors are raised for correct data. - mapping_dict = { - 'exp_id': 'exp_id1', - 'exp_version': 2, - 'state_name': u'網站有中', - 'job_id': 'job_id1' - } - mapping = self._get_mapping_from_dict(mapping_dict) + def test_validation_for_mapping_with_correct_data(self): + mapping = self._get_mapping_from_dict(self.mapping_dict) mapping.validate() - # Verify validation error is raised when int is provided for exp_id - # instead of string. - mapping_dict['exp_id'] = 1 - mapping = self._get_mapping_from_dict(mapping_dict) + def test_validation_with_invalid_exp_id(self): + self.mapping_dict['exp_id'] = 1 + mapping = self._get_mapping_from_dict(self.mapping_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Expected exp_id to be a string'): mapping.validate() - # Verify validation error is raised when string is provided for - # exp_version instead of int. - mapping_dict['exp_id'] = 'exp_id1' - mapping_dict['exp_version'] = '1' - mapping = self._get_mapping_from_dict(mapping_dict) + def test_validation_with_invalid_exp_version(self): + self.mapping_dict['exp_version'] = '1' + mapping = self._get_mapping_from_dict(self.mapping_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Expected exp_version to be an int'): mapping.validate() - # Verify validation error is raised when int is provided for job id - # instead of string. - mapping_dict['exp_version'] = 2 - mapping_dict['job_id'] = 0 - mapping = self._get_mapping_from_dict(mapping_dict) + def test_validation_with_invalid_job_id(self): + self.mapping_dict['job_id'] = 0 + mapping = self._get_mapping_from_dict(self.mapping_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Expected job_id to be a string'): mapping.validate() - # Verify validation error is raised when int is provided for state name - # instead of string. - mapping_dict['job_id'] = 'job_id1' - mapping_dict['state_name'] = 0 - mapping = self._get_mapping_from_dict(mapping_dict) + def test_validation_with_invalid_state_name(self): + self.mapping_dict['state_name'] = 0 + mapping = self._get_mapping_from_dict(self.mapping_dict) with self.assertRaisesRegexp( utils.ValidationError, 'Expected state_name to be a string'): mapping.validate() diff --git a/core/domain/stats_domain_test.py b/core/domain/stats_domain_test.py index 540cb7b14f58..9ce62b96a888 100644 --- a/core/domain/stats_domain_test.py +++ b/core/domain/stats_domain_test.py @@ -31,6 +31,40 @@ class ExplorationStatsTests(test_utils.GenericTestBase): """Tests the ExplorationStats domain object.""" + def setUp(self): + super(ExplorationStatsTests, self).setUp() + + self.state_stats_dict = { + 'total_answers_count_v1': 0, + 'total_answers_count_v2': 10, + 'useful_feedback_count_v1': 0, + 'useful_feedback_count_v2': 4, + 'total_hit_count_v1': 0, + 'total_hit_count_v2': 18, + 'first_hit_count_v1': 0, + 'first_hit_count_v2': 7, + 'num_times_solution_viewed_v2': 2, + 'num_completions_v1': 0, + 'num_completions_v2': 2 + } + + self.exploration_stats_dict = { + 'exp_id': 'exp_id1', + 'exp_version': 1, + 'num_starts_v1': 0, + 'num_starts_v2': 30, + 'num_actual_starts_v1': 0, + 'num_actual_starts_v2': 10, + 'num_completions_v1': 0, + 'num_completions_v2': 5, + 'state_stats_mapping': { + 'Home': self.state_stats_dict, + 'Home2': self.state_stats_dict + } + } + + self.exploration_stats = self._get_exploration_stats_from_dict( + self.exploration_stats_dict) def _get_exploration_stats_from_dict(self, exploration_stats_dict): """Converts and returns the ExplorationStats object from the given @@ -52,19 +86,6 @@ def _get_exploration_stats_from_dict(self, exploration_stats_dict): state_stats_mapping) def test_to_dict(self): - state_stats_dict = { - 'total_answers_count_v1': 0, - 'total_answers_count_v2': 10, - 'useful_feedback_count_v1': 0, - 'useful_feedback_count_v2': 4, - 'total_hit_count_v1': 0, - 'total_hit_count_v2': 18, - 'first_hit_count_v1': 0, - 'first_hit_count_v2': 7, - 'num_times_solution_viewed_v2': 2, - 'num_completions_v1': 0, - 'num_completions_v2': 2 - } expected_exploration_stats_dict = { 'exp_id': 'exp_id1', 'exp_version': 1, @@ -75,7 +96,7 @@ def test_to_dict(self): 'num_completions_v1': 0, 'num_completions_v2': 5, 'state_stats_mapping': { - 'Home': state_stats_dict + 'Home': self.state_stats_dict } } observed_exploration_stats = self._get_exploration_stats_from_dict( @@ -86,132 +107,41 @@ def test_to_dict(self): def test_get_sum_of_first_hit_counts(self): """Test the get_sum_of_first_hit_counts method.""" - state_stats_dict = { - 'total_answers_count_v1': 0, - 'total_answers_count_v2': 10, - 'useful_feedback_count_v1': 0, - 'useful_feedback_count_v2': 4, - 'total_hit_count_v1': 0, - 'total_hit_count_v2': 18, - 'first_hit_count_v1': 0, - 'first_hit_count_v2': 7, - 'num_times_solution_viewed_v2': 2, - 'num_completions_v1': 0, - 'num_completions_v2': 2 - } - exploration_stats_dict = { - 'exp_id': 'exp_id1', - 'exp_version': 1, - 'num_starts_v1': 0, - 'num_starts_v2': 30, - 'num_actual_starts_v1': 0, - 'num_actual_starts_v2': 10, - 'num_completions_v1': 0, - 'num_completions_v2': 5, - 'state_stats_mapping': { - 'Home': state_stats_dict, - 'Home2': state_stats_dict - } - } - exploration_stats = self._get_exploration_stats_from_dict( - exploration_stats_dict) + self.assertEqual( + self.exploration_stats.get_sum_of_first_hit_counts(), 14) - self.assertEqual(exploration_stats.get_sum_of_first_hit_counts(), 14) + def test_validate_for_exploration_stats_with_correct_data(self): + self.exploration_stats.validate() - def test_validate(self): - state_stats_dict = { - 'total_answers_count_v1': 0, - 'total_answers_count_v2': 10, - 'useful_feedback_count_v1': 0, - 'useful_feedback_count_v2': 4, - 'total_hit_count_v1': 0, - 'total_hit_count_v2': 18, - 'first_hit_count_v1': 0, - 'first_hit_count_v2': 7, - 'num_times_solution_viewed_v2': 2, - 'num_completions_v1': 0, - 'num_completions_v2': 2 - } - exploration_stats_dict = { - 'exp_id': 'exp_id1', - 'exp_version': 1, - 'num_starts_v1': 0, - 'num_starts_v2': 30, - 'num_actual_starts_v1': 0, - 'num_actual_starts_v2': 10, - 'num_completions_v1': 0, - 'num_completions_v2': 5, - 'state_stats_mapping': { - 'Home': state_stats_dict - } - } - exploration_stats = self._get_exploration_stats_from_dict( - exploration_stats_dict) - exploration_stats.validate() - - # Make the exp_id integer. - exploration_stats.exp_id = 10 + def test_validate_with_int_exp_id(self): + self.exploration_stats.exp_id = 10 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected exp_id to be a string')): - exploration_stats.validate() + self.exploration_stats.validate() - # Make the num_actual_starts string. - exploration_stats.exp_id = 'exp_id1' - exploration_stats.num_actual_starts_v2 = '0' + def test_validation_with_string_num_actual_starts(self): + self.exploration_stats.num_actual_starts_v2 = '0' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected num_actual_starts_v2 to be an int')): - exploration_stats.validate() + self.exploration_stats.validate() - # Make the state_stats_mapping list. - exploration_stats.num_actual_starts_v2 = 10 - exploration_stats.state_stats_mapping = [] + def test_validation_with_list_state_stats_mapping(self): + self.exploration_stats.state_stats_mapping = [] with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected state_stats_mapping to be a dict')): - exploration_stats.validate() + self.exploration_stats.validate() - # Make the num_completions negative. - exploration_stats.state_stats_mapping = {} - exploration_stats.num_completions_v2 = -5 + def test_validation_with_negative_num_completions(self): + self.exploration_stats.num_completions_v2 = -5 with self.assertRaisesRegexp(utils.ValidationError, ( '%s cannot have negative values' % ('num_completions_v2'))): - exploration_stats.validate() + self.exploration_stats.validate() def test_validate_exp_version(self): - state_stats_dict = { - 'total_answers_count_v1': 0, - 'total_answers_count_v2': 10, - 'useful_feedback_count_v1': 0, - 'useful_feedback_count_v2': 4, - 'total_hit_count_v1': 0, - 'total_hit_count_v2': 18, - 'first_hit_count_v1': 0, - 'first_hit_count_v2': 7, - 'num_times_solution_viewed_v2': 2, - 'num_completions_v1': 0, - 'num_completions_v2': 2 - } - exploration_stats_dict = { - 'exp_id': 'exp_id1', - 'exp_version': 1, - 'num_starts_v1': 0, - 'num_starts_v2': 30, - 'num_actual_starts_v1': 0, - 'num_actual_starts_v2': 10, - 'num_completions_v1': 0, - 'num_completions_v2': 5, - 'state_stats_mapping': { - 'Home': state_stats_dict - } - } - - exploration_stats = self._get_exploration_stats_from_dict( - exploration_stats_dict) - exploration_stats.validate() - - exploration_stats.exp_version = 'invalid_exp_version' + self.exploration_stats.exp_version = 'invalid_exp_version' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected exp_version to be an int')): - exploration_stats.validate() + self.exploration_stats.validate() def test_to_frontend_dict(self): state_stats_dict = { @@ -270,6 +200,11 @@ def test_to_frontend_dict(self): class StateStatsTests(test_utils.GenericTestBase): """Tests the StateStats domain object.""" + def setUp(self): + super(StateStatsTests, self).setUp() + + self.state_stats = stats_domain.StateStats( + 0, 10, 0, 4, 0, 18, 0, 7, 2, 0, 2) def test_from_dict(self): state_stats_dict = { @@ -353,21 +288,20 @@ def test_to_dict(self): state_stats = stats_domain.StateStats(0, 10, 0, 4, 0, 18, 0, 7, 2, 0, 2) self.assertEqual(state_stats_dict, state_stats.to_dict()) - def test_validation(self): - state_stats = stats_domain.StateStats(0, 10, 0, 4, 0, 18, 0, 7, 2, 0, 2) - state_stats.validate() + def test_validation_for_state_stats_with_correct_data(self): + self.state_stats.validate() - # Change total_answers_count to string. - state_stats.total_answers_count_v2 = '10' + def test_validation_for_state_stats_with_string_total_answers_count(self): + self.state_stats.total_answers_count_v2 = '10' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected total_answers_count_v2 to be an int')): - state_stats.validate() + self.state_stats.validate() - # Make the total_answers_count negative. - state_stats.total_answers_count_v2 = -5 + def test_validation_for_state_stats_with_negative_total_answers_count(self): + self.state_stats.total_answers_count_v2 = -5 with self.assertRaisesRegexp(utils.ValidationError, ( '%s cannot have negative values' % ('total_answers_count_v2'))): - state_stats.validate() + self.state_stats.validate() def test_to_frontend_dict(self): state_stats_dict = { @@ -401,15 +335,10 @@ def test_to_frontend_dict(self): class ExplorationIssuesTests(test_utils.GenericTestBase): """Tests the ExplorationIssues domain object.""" + def setUp(self): + super(ExplorationIssuesTests, self).setUp() - def test_create_default(self): - exp_issues = stats_domain.ExplorationIssues.create_default('exp_id1', 1) - self.assertEqual(exp_issues.exp_id, 'exp_id1') - self.assertEqual(exp_issues.exp_version, 1) - self.assertEqual(exp_issues.unresolved_issues, []) - - def test_to_dict(self): - exp_issues = stats_domain.ExplorationIssues( + self.exp_issues = stats_domain.ExplorationIssues( 'exp_id1', 1, [ stats_domain.ExplorationIssue.from_dict({ 'issue_type': 'EarlyQuit', @@ -426,7 +355,14 @@ def test_to_dict(self): 'is_valid': True}) ]) - exp_issues_dict = exp_issues.to_dict() + def test_create_default(self): + exp_issues = stats_domain.ExplorationIssues.create_default('exp_id1', 1) + self.assertEqual(exp_issues.exp_id, 'exp_id1') + self.assertEqual(exp_issues.exp_version, 1) + self.assertEqual(exp_issues.unresolved_issues, []) + + def test_to_dict(self): + exp_issues_dict = self.exp_issues.to_dict() self.assertEqual(exp_issues_dict['exp_id'], 'exp_id1') self.assertEqual(exp_issues_dict['exp_version'], 1) @@ -486,84 +422,36 @@ def test_from_dict(self): 'schema_version': 1, 'is_valid': True}) - def test_validate(self): - exp_issues = stats_domain.ExplorationIssues( - 'exp_id1', 1, [ - stats_domain.ExplorationIssue.from_dict({ - 'issue_type': 'EarlyQuit', - 'issue_customization_args': { - 'state_name': { - 'value': 'state_name1' - }, - 'time_spent_in_exp_in_msecs': { - 'value': 200 - } - }, - 'playthrough_ids': ['playthrough_id1'], - 'schema_version': 1, - 'is_valid': True}) - ]) - exp_issues.validate() + def test_validate_for_exp_issues_with_correct_data(self): + self.exp_issues.validate() - # Change ID to int. - exp_issues.exp_id = 5 + def test_validate_with_int_exp_id(self): + self.exp_issues.exp_id = 5 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected exp_id to be a string, received %s' % (type(5)))): - exp_issues.validate() + self.exp_issues.validate() def test_validate_exp_version(self): - exp_issues = stats_domain.ExplorationIssues( - 'exp_id1', 1, [ - stats_domain.ExplorationIssue.from_dict({ - 'issue_type': 'EarlyQuit', - 'issue_customization_args': { - 'state_name': { - 'value': 'state_name1' - }, - 'time_spent_in_exp_in_msecs': { - 'value': 200 - } - }, - 'playthrough_ids': ['playthrough_id1'], - 'schema_version': 1, - 'is_valid': True}) - ]) - exp_issues.validate() - - exp_issues.exp_version = 'invalid_version' + self.exp_issues.exp_version = 'invalid_version' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected exp_version to be an int')): - exp_issues.validate() + self.exp_issues.validate() def test_validate_unresolved_issues(self): - exp_issues = stats_domain.ExplorationIssues( - 'exp_id1', 1, [ - stats_domain.ExplorationIssue.from_dict({ - 'issue_type': 'EarlyQuit', - 'issue_customization_args': { - 'state_name': { - 'value': 'state_name1' - }, - 'time_spent_in_exp_in_msecs': { - 'value': 200 - } - }, - 'playthrough_ids': ['playthrough_id1'], - 'schema_version': 1, - 'is_valid': True}) - ]) - exp_issues.validate() - - exp_issues.unresolved_issues = 0 + self.exp_issues.unresolved_issues = 0 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected unresolved_issues to be a list')): - exp_issues.validate() + self.exp_issues.validate() class PlaythroughTests(test_utils.GenericTestBase): """Tests the Playthrough domain object.""" + def setUp(self): + super(PlaythroughTests, self).setUp() + + self.playthrough = self._get_valid_early_quit_playthrough() def _get_valid_early_quit_playthrough(self): """Returns an early quit playthrough after validating it.""" @@ -696,25 +584,20 @@ def test_from_backend_dict(self): 'exp_id not in playthrough data dict.'): stats_domain.Playthrough.from_backend_dict(playthrough_dict) - def test_validate(self): - playthrough = self._get_valid_early_quit_playthrough() - - # Change exp_version to string. - playthrough.exp_version = '1' + def test_validate_with_string_exp_version(self): + self.playthrough.exp_version = '1' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected exp_version to be an int, received %s' % (type('1')))): - playthrough.validate() + self.playthrough.validate() - # Change to invalid issue_type. - playthrough.exp_version = 1 - playthrough.issue_type = 'InvalidIssueType' + def test_validate_with_invalid_issue_type(self): + self.playthrough.issue_type = 'InvalidIssueType' with self.assertRaisesRegexp(utils.ValidationError, ( - 'Invalid issue type: %s' % playthrough.issue_type)): - playthrough.validate() + 'Invalid issue type: %s' % self.playthrough.issue_type)): + self.playthrough.validate() - # Change to invalid action_type. - playthrough.issue_type = 'EarlyQuit' - playthrough.actions = [ + def test_validate_with_invalid_action_type(self): + self.playthrough.actions = [ stats_domain.LearnerAction.from_dict({ 'action_type': 'InvalidActionType', 'schema_version': 1, @@ -726,47 +609,44 @@ def test_validate(self): })] with self.assertRaisesRegexp(utils.ValidationError, ( 'Invalid action type: %s' % 'InvalidActionType')): - playthrough.validate() + self.playthrough.validate() def test_validate_non_str_exp_id(self): - playthrough = self._get_valid_early_quit_playthrough() - - playthrough.exp_id = 0 + self.playthrough.exp_id = 0 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected exp_id to be a string')): - playthrough.validate() + self.playthrough.validate() def test_validate_non_str_issue_type(self): - playthrough = self._get_valid_early_quit_playthrough() - - playthrough.issue_type = 0 + self.playthrough.issue_type = 0 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected issue_type to be a string')): - playthrough.validate() + self.playthrough.validate() def test_validate_non_list_actions(self): - playthrough = self._get_valid_early_quit_playthrough() - - playthrough.actions = 0 + self.playthrough.actions = 0 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected actions to be a list')): - playthrough.validate() + self.playthrough.validate() def test_validate_non_dict_issue_customization_args(self): - playthrough = self._get_valid_early_quit_playthrough() - - playthrough.issue_customization_args = 0 + self.playthrough.issue_customization_args = 0 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected issue_customization_args to be a dict')): - playthrough.validate() + self.playthrough.validate() class ExplorationIssueTests(test_utils.GenericTestBase): """Tests the ExplorationIssue domain object.""" + def setUp(self): + super(ExplorationIssueTests, self).setUp() + + self.exp_issue = stats_domain.ExplorationIssue( + 'EarlyQuit', {}, [], 1, True) def _dummy_convert_issue_v1_dict_to_v2_dict(self, issue_dict): """A test implementation of schema conversion function.""" @@ -902,54 +782,48 @@ def test_actual_update_exp_issue_from_model_raises_error(self): stats_domain.ExplorationIssue.update_exp_issue_from_model( exp_issue_dict) - def test_validate(self): - exp_issue = stats_domain.ExplorationIssue('EarlyQuit', {}, [], 1, True) - exp_issue.validate() + def test_validate_for_exp_issues_with_correct_data(self): + self.exp_issue.validate() - # Change issue_type to int. - exp_issue.issue_type = 5 + def test_validate_with_int_issue_type(self): + self.exp_issue.issue_type = 5 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected issue_type to be a string, received %s' % (type(5)))): - exp_issue.validate() + self.exp_issue.validate() - # Change schema_version to string. - exp_issue.issue_type = 'EarlyQuit' - exp_issue.schema_version = '1' + def test_validate_with_string_schema_version(self): + self.exp_issue.schema_version = '1' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected schema_version to be an int, received %s' % (type('1')))): - exp_issue.validate() + self.exp_issue.validate() def test_validate_issue_type(self): - exp_issue = stats_domain.ExplorationIssue('EarlyQuit', {}, [], 1, True) - exp_issue.validate() - - exp_issue.issue_type = 'invalid_issue_type' + self.exp_issue.issue_type = 'invalid_issue_type' with self.assertRaisesRegexp(utils.ValidationError, ( 'Invalid issue type')): - exp_issue.validate() + self.exp_issue.validate() def test_validate_playthrough_ids(self): - exp_issue = stats_domain.ExplorationIssue('EarlyQuit', {}, [], 1, True) - exp_issue.validate() - - exp_issue.playthrough_ids = 'invalid_playthrough_ids' + self.exp_issue.playthrough_ids = 'invalid_playthrough_ids' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected playthrough_ids to be a list')): - exp_issue.validate() + self.exp_issue.validate() def test_validate_playthrough_id_type(self): - exp_issue = stats_domain.ExplorationIssue('EarlyQuit', {}, [], 1, True) - exp_issue.validate() - - exp_issue.playthrough_ids = [0, 1] + self.exp_issue.playthrough_ids = [0, 1] with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected each playthrough_id to be a string')): - exp_issue.validate() + self.exp_issue.validate() class LearnerActionTests(test_utils.GenericTestBase): """Tests the LearnerAction domain object.""" + def setUp(self): + super(LearnerActionTests, self).setUp() + + self.learner_action = stats_domain.LearnerAction( + 'ExplorationStart', {}, 1) def _dummy_convert_action_v1_dict_to_v2_dict(self, action_dict): """A test implementation of schema conversion function.""" @@ -1092,22 +966,20 @@ def test_actual_update_learner_action_from_model_raises_error(self): stats_domain.LearnerAction.update_learner_action_from_model( learner_action_dict) - def test_validate(self): - learner_action = stats_domain.LearnerAction('ExplorationStart', {}, 1) - learner_action.validate() + def test_validate_for_learner_action_with_correct_data(self): + self.learner_action.validate() - # Change action_type to int. - learner_action.action_type = 5 + def test_validate_with_int_action_type(self): + self.learner_action.action_type = 5 with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected action_type to be a string, received %s' % (type(5)))): - learner_action.validate() + self.learner_action.validate() - # Change schema_version to string. - learner_action.action_type = 'EarlyQuit' - learner_action.schema_version = '1' + def test_validate_with_string_schema_version(self): + self.learner_action.schema_version = '1' with self.assertRaisesRegexp(utils.ValidationError, ( 'Expected schema_version to be an int, received %s' % (type('1')))): - learner_action.validate() + self.learner_action.validate() class StateAnswersTests(test_utils.GenericTestBase): diff --git a/core/domain/visualization_registry_test.py b/core/domain/visualization_registry_test.py index 45286cb5eafa..21a3369d78c3 100644 --- a/core/domain/visualization_registry_test.py +++ b/core/domain/visualization_registry_test.py @@ -19,6 +19,7 @@ import importlib import inspect import os +import re from core.domain import visualization_registry from core.tests import test_utils @@ -39,6 +40,34 @@ def test_get_visualization_class_with_invalid_id_raises_error(self): visualization_registry.Registry.get_visualization_class( 'invalid_visualization_id') + def test_visualization_class_with_invalid_option_names(self): + bar_chart = visualization_registry.Registry.get_visualization_class( + 'BarChart') + bar_chart_instance = bar_chart('AnswerFrequencies', {}, True) + + with self.assertRaisesRegexp( + Exception, + re.escape( + 'For visualization BarChart, expected option names ' + '[\'x_axis_label\', \'y_axis_label\']; received names []')): + bar_chart_instance.validate() + + def test_visualization_class_with_invalid_addressed_info_is_supported(self): + bar_chart = visualization_registry.Registry.get_visualization_class( + 'BarChart') + option_names = { + 'x_axis_label': 'Answer', + 'y_axis_label': 'Count' + } + bar_chart_instance = bar_chart( + 'AnswerFrequencies', option_names, 'invalid_value') + + with self.assertRaisesRegexp( + Exception, + 'For visualization BarChart, expected a bool value for ' + 'addressed_info_is_supported; received invalid_value'): + bar_chart_instance.validate() + class VisualizationsNameTests(test_utils.GenericTestBase): diff --git a/core/templates/dev/head/base_components/BaseContentDirective.ts b/core/templates/dev/head/base_components/BaseContentDirective.ts index e4b97c8498e7..b3144746d85a 100644 --- a/core/templates/dev/head/base_components/BaseContentDirective.ts +++ b/core/templates/dev/head/base_components/BaseContentDirective.ts @@ -17,7 +17,7 @@ */ require('pages/OppiaFooterDirective.ts'); - +require('domain/sidebar/SidebarStatusService.ts'); require('domain/utilities/UrlInterpolationService.ts'); var oppia = require('AppInit.ts').module; @@ -39,7 +39,11 @@ oppia.directive('baseContent', [ '/base_components/base_content_directive.html'), controllerAs: '$ctrl', controller: [ - function() {} + '$scope', 'SidebarStatusService', + function($scope, SidebarStatusService) { + var ctrl = this; + ctrl.isSidebarShown = SidebarStatusService.isSidebarShown; + } ] }; } diff --git a/core/templates/dev/head/components/question-directives/questions-list/questions-list.constants.ts b/core/templates/dev/head/components/question-directives/questions-list/questions-list.constants.ts new file mode 100644 index 000000000000..c402d1cc8dbc --- /dev/null +++ b/core/templates/dev/head/components/question-directives/questions-list/questions-list.constants.ts @@ -0,0 +1,23 @@ +// Copyright 2019 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Constants for the question player directive. + */ + +var oppia = require('AppInit.ts').module; + +oppia.constant('DEFAULT_SKILL_DIFFICULTY', '0.3'); +oppia.constant('MODE_SELECT_DIFFICULTY', 'MODE_SELECT_DIFFICULTY'); +oppia.constant('MODE_SELECT_SKILL', 'MODE_SELECT_SKILL'); diff --git a/core/templates/dev/head/components/question-directives/questions-list/questions-list.directive.ts b/core/templates/dev/head/components/question-directives/questions-list/questions-list.directive.ts index 2ce065a33a9d..d7bc0bd906fc 100644 --- a/core/templates/dev/head/components/question-directives/questions-list/questions-list.directive.ts +++ b/core/templates/dev/head/components/question-directives/questions-list/questions-list.directive.ts @@ -21,6 +21,9 @@ require('directives/AngularHtmlBindDirective.ts'); require( 'components/question-directives/question-editor/' + 'question-editor.directive.ts'); +require( + 'components/question-directives/questions-list/' + + 'questions-list.constants.ts'); require('components/entity-creation-services/question-creation.service.ts'); require('domain/editor/undo_redo/UndoRedoService.ts'); @@ -28,6 +31,7 @@ require('domain/question/EditableQuestionBackendApiService.ts'); require('domain/question/QuestionObjectFactory.ts'); require('domain/skill/EditableSkillBackendApiService.ts'); require('domain/skill/MisconceptionObjectFactory.ts'); +require('domain/skill/SkillDifficultyObjectFactory.ts'); require('domain/utilities/UrlInterpolationService.ts'); require('filters/string-utility-filters/truncate.filter.ts'); require('pages/topic-editor-page/services/topic-editor-state.service.ts'); @@ -62,14 +66,18 @@ oppia.directive('questionsList', [ 'AlertsService', 'QuestionCreationService', 'UrlService', 'NUM_QUESTIONS_PER_PAGE', 'EditableQuestionBackendApiService', 'EditableSkillBackendApiService', 'MisconceptionObjectFactory', - 'QuestionObjectFactory', 'EVENT_QUESTION_SUMMARIES_INITIALIZED', + 'QuestionObjectFactory', 'SkillDifficultyObjectFactory', + 'DEFAULT_SKILL_DIFFICULTY', 'EVENT_QUESTION_SUMMARIES_INITIALIZED', + 'MODE_SELECT_DIFFICULTY', 'MODE_SELECT_SKILL', 'StateEditorService', 'QuestionUndoRedoService', 'UndoRedoService', function( $scope, $filter, $http, $q, $uibModal, $window, AlertsService, QuestionCreationService, UrlService, NUM_QUESTIONS_PER_PAGE, EditableQuestionBackendApiService, EditableSkillBackendApiService, MisconceptionObjectFactory, - QuestionObjectFactory, EVENT_QUESTION_SUMMARIES_INITIALIZED, + QuestionObjectFactory, SkillDifficultyObjectFactory, + DEFAULT_SKILL_DIFFICULTY, EVENT_QUESTION_SUMMARIES_INITIALIZED, + MODE_SELECT_DIFFICULTY, MODE_SELECT_SKILL, StateEditorService, QuestionUndoRedoService, UndoRedoService) { var ctrl = this; ctrl.currentPage = 0; @@ -133,7 +141,8 @@ oppia.directive('questionsList', [ } if (!ctrl.questionIsBeingUpdated) { EditableQuestionBackendApiService.createQuestion( - ctrl.newQuestionSkillIds, ctrl.question.toBackendDict(true) + ctrl.newQuestionSkillIds, ctrl.newQuestionSkillDifficulties, + ctrl.question.toBackendDict(true) ).then(function() { ctrl.questionSummaries = ctrl.getQuestionSummariesAsync( 0, ctrl.skillIds, true, true @@ -173,14 +182,18 @@ oppia.directive('questionsList', [ }; ctrl.createQuestion = function() { + ctrl.newQuestionSkillIds = []; + var currentMode = MODE_SELECT_SKILL; if (!ctrl.selectSkillModalIsShown()) { ctrl.newQuestionSkillIds = ctrl.skillIds; - ctrl.populateMisconceptions(ctrl.skillIds); - if (AlertsService.warnings.length === 0) { - ctrl.initializeNewQuestionCreation(ctrl.skillIds); - } - return; + currentMode = MODE_SELECT_DIFFICULTY; } + var linkedSkillsWithDifficulty = []; + ctrl.newQuestionSkillIds.forEach(function(skillId) { + linkedSkillsWithDifficulty.push( + SkillDifficultyObjectFactory.create( + skillId, '', DEFAULT_SKILL_DIFFICULTY)); + }); var allSkillSummaries = ctrl.getAllSkillSummaries(); var modalInstance = $uibModal.open({ templateUrl: UrlInterpolationService.getDirectiveTemplateUrl( @@ -190,43 +203,71 @@ oppia.directive('questionsList', [ controller: [ '$scope', '$uibModalInstance', function($scope, $uibModalInstance) { - $scope.selectedSkillIds = []; - $scope.skillSummaries = allSkillSummaries; - $scope.skillSummaries.forEach(function(summary) { - summary.isSelected = false; - }); + var init = function() { + $scope.currentMode = currentMode; + $scope.linkedSkillsWithDifficulty = + linkedSkillsWithDifficulty; + $scope.skillSummaries = allSkillSummaries; + }; $scope.selectOrDeselectSkill = function(skillId, index) { if (!$scope.skillSummaries[index].isSelected) { - $scope.selectedSkillIds.push(skillId); + $scope.linkedSkillsWithDifficulty.push( + SkillDifficultyObjectFactory.create( + skillId, + $scope.skillSummaries[index].getDescription(), + DEFAULT_SKILL_DIFFICULTY)); $scope.skillSummaries[index].isSelected = true; } else { - var idIndex = $scope.selectedSkillIds.indexOf(skillId); - $scope.selectedSkillIds.splice(idIndex, 1); + var idIndex = $scope.linkedSkillsWithDifficulty.map( + function(linkedSkillWithDifficulty) { + return linkedSkillWithDifficulty.getId(); + }).indexOf(skillId); + $scope.linkedSkillsWithDifficulty.splice(idIndex, 1); $scope.skillSummaries[index].isSelected = false; } }; - $scope.done = function() { - $uibModalInstance.close($scope.selectedSkillIds); + $scope.goToSelectSkillView = function() { + $scope.currentMode = MODE_SELECT_SKILL; }; - $scope.cancel = function() { + $scope.goToSelectDifficultyView = function() { + $scope.currentMode = MODE_SELECT_DIFFICULTY; + }; + + $scope.startQuestionCreation = function() { + $uibModalInstance.close($scope.linkedSkillsWithDifficulty); + }; + + $scope.cancelModal = function() { $uibModalInstance.dismiss('cancel'); }; - $scope.ok = function() { + $scope.closeModal = function() { $uibModalInstance.dismiss('ok'); }; + + init(); } ] }); - modalInstance.result.then(function(skillIds) { - ctrl.newQuestionSkillIds = skillIds; - ctrl.populateMisconceptions(skillIds); + modalInstance.result.then(function(linkedSkillsWithDifficulty) { + ctrl.newQuestionSkillIds = []; + ctrl.newQuestionSkillDifficulties = []; + linkedSkillsWithDifficulty.forEach( + function(linkedSkillWithDifficulty) { + ctrl.newQuestionSkillIds.push( + linkedSkillWithDifficulty.getId()); + ctrl.newQuestionSkillDifficulties.push( + linkedSkillWithDifficulty.getDifficulty()); + } + ); + ctrl.populateMisconceptions(ctrl.newQuestionSkillIds); if (AlertsService.warnings.length === 0) { - ctrl.initializeNewQuestionCreation(skillIds); + ctrl.initializeNewQuestionCreation( + ctrl.newQuestionSkillIds); } }); }; diff --git a/core/templates/dev/head/css/oppia.css b/core/templates/dev/head/css/oppia.css index 6b9cabd4f532..71dafd5d77b5 100644 --- a/core/templates/dev/head/css/oppia.css +++ b/core/templates/dev/head/css/oppia.css @@ -3890,7 +3890,7 @@ span.learner-action { margin-bottom: 2em; } -span.oppia-issues-learner-action { +div.oppia-issues-learner-action { display: block; margin-bottom: 2em; } diff --git a/core/templates/dev/head/domain/question/EditableQuestionBackendApiService.ts b/core/templates/dev/head/domain/question/EditableQuestionBackendApiService.ts index 4a9fb833725c..a5fa16789758 100644 --- a/core/templates/dev/head/domain/question/EditableQuestionBackendApiService.ts +++ b/core/templates/dev/head/domain/question/EditableQuestionBackendApiService.ts @@ -39,15 +39,14 @@ oppia.factory('EditableQuestionBackendApiService', [ EDITABLE_QUESTION_DATA_URL_TEMPLATE, QUESTION_CREATION_URL, QUESTION_SKILL_LINK_URL_TEMPLATE) { var _createQuestion = function( - skillIds, questionDict, successCallback, errorCallback) { - var questionCreationUrl = UrlInterpolationService.interpolateUrl( - QUESTION_CREATION_URL, { - comma_separated_skill_ids: skillIds.join(',') - }); + skillIds, skillDifficulties, + questionDict, successCallback, errorCallback) { var postData = { - question_dict: questionDict + question_dict: questionDict, + skill_ids: skillIds, + skill_difficulties: skillDifficulties }; - $http.post(questionCreationUrl, postData).then(function(response) { + $http.post(QUESTION_CREATION_URL, postData).then(function(response) { var questionId = response.data.question_id; if (successCallback) { successCallback(questionId); @@ -126,9 +125,10 @@ oppia.factory('EditableQuestionBackendApiService', [ }; return { - createQuestion: function(skillIds, questionDict) { + createQuestion: function(skillIds, skillDifficulties, questionDict) { return $q(function(resolve, reject) { - _createQuestion(skillIds, questionDict, resolve, reject); + _createQuestion(skillIds, skillDifficulties, + questionDict, resolve, reject); }); }, diff --git a/core/templates/dev/head/domain/question/question-domain.constants.ts b/core/templates/dev/head/domain/question/question-domain.constants.ts index fe8c43c543a5..63c3c5c10eef 100644 --- a/core/templates/dev/head/domain/question/question-domain.constants.ts +++ b/core/templates/dev/head/domain/question/question-domain.constants.ts @@ -23,7 +23,7 @@ oppia.constant( '/question_editor_handler/data/'); oppia.constant( 'QUESTION_CREATION_URL', - '/question_editor_handler/create_new/'); + '/question_editor_handler/create_new'); oppia.constant( 'QUESTION_SKILL_LINK_URL_TEMPLATE', '/manage_question_skill_link//'); diff --git a/core/templates/dev/head/domain/skill/SkillDifficultyObjectFactory.ts b/core/templates/dev/head/domain/skill/SkillDifficultyObjectFactory.ts new file mode 100644 index 000000000000..bfed7b26412e --- /dev/null +++ b/core/templates/dev/head/domain/skill/SkillDifficultyObjectFactory.ts @@ -0,0 +1,66 @@ +// Copyright 2018 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Object factory for creating frontend instances of + * skills with their difficulty for a question. + */ + +var oppia = require('AppInit.ts').module; + +oppia.factory('SkillDifficultyObjectFactory', [ + function() { + var SkillDifficulty = function(id, description, difficulty) { + this._id = id; + this._description = description; + this._difficulty = difficulty; + }; + + SkillDifficulty.prototype.toBackendDict = function() { + return { + id: this._id, + description: this._description, + difficulty: this._difficulty, + }; + }; + + /* eslint-disable dot-notation */ + SkillDifficulty['create'] = function(id, description, difficulty) { + /* eslint-enable dot-notation */ + return new SkillDifficulty(id, description, difficulty); + }; + + SkillDifficulty.prototype.getId = function() { + return this._id; + }; + + SkillDifficulty.prototype.getDescription = function() { + return this._description; + }; + + SkillDifficulty.prototype.setDescription = function(newDescription) { + this._description = newDescription; + }; + + SkillDifficulty.prototype.getDifficulty = function() { + return this._difficulty; + }; + + SkillDifficulty.prototype.setDifficulty = function(newDifficulty) { + this._difficulty = newDifficulty; + }; + + return SkillDifficulty; + } +]); diff --git a/core/templates/dev/head/domain/skill/SkillDifficultyObjectFactorySpec.ts b/core/templates/dev/head/domain/skill/SkillDifficultyObjectFactorySpec.ts new file mode 100644 index 000000000000..380f0aad7ea1 --- /dev/null +++ b/core/templates/dev/head/domain/skill/SkillDifficultyObjectFactorySpec.ts @@ -0,0 +1,51 @@ +// Copyright 2018 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Unit tests for SkillDifficultyObjectFactory. + */ + +require('domain/skill/SkillDifficultyObjectFactory.ts'); + +describe('Skill Difficulty object factory', function() { + beforeEach(angular.mock.module('oppia')); + + describe('SkillDifficultyObjectFactory', function() { + var SkillDifficultyObjectFactory; + + beforeEach(angular.mock.inject(function($injector) { + SkillDifficultyObjectFactory = $injector.get( + 'SkillDifficultyObjectFactory'); + })); + + it('should create a new skill difficulty instance', function() { + var skillDifficulty = + SkillDifficultyObjectFactory.create('1', 'test skill', 0.3); + expect(skillDifficulty.getId()).toEqual('1'); + expect(skillDifficulty.getDescription()).toEqual('test skill'); + expect(skillDifficulty.getDifficulty()).toEqual(0.3); + }); + + it('should convert to a backend dictionary', function() { + var skillDifficulty = + SkillDifficultyObjectFactory.create('1', 'test skill', 0.3); + var skillDifficultyDict = { + id: '1', + description: 'test skill', + difficulty: 0.3 + }; + expect(skillDifficulty.toBackendDict()).toEqual(skillDifficultyDict); + }); + }); +}); diff --git a/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactory.ts b/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactory.ts index f6ee18ea1757..cb99c6b6bf80 100644 --- a/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactory.ts +++ b/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactory.ts @@ -16,25 +16,30 @@ * @fileoverview Factory for creating Feedback Cards in the Improvements Tab. */ - require('domain/statistics/ImprovementActionButtonObjectFactory.ts'); -require('domain/utilities/UrlInterpolationService.ts'); +require( + 'pages/exploration-editor-page/improvements-tab/services/' + + 'improvement-modal.service.ts'); require( 'pages/exploration-editor-page/feedback-tab/services/thread-data.service.ts'); - require('domain/statistics/statistics-domain.constants.ts'); var oppia = require('AppInit.ts').module; oppia.factory('FeedbackImprovementCardObjectFactory', [ - '$q', 'ImprovementActionButtonObjectFactory', 'ThreadDataService', - 'UrlInterpolationService', 'FEEDBACK_IMPROVEMENT_CARD_TYPE', + '$q', 'ImprovementActionButtonObjectFactory', 'ImprovementModalService', + 'ThreadDataService', 'FEEDBACK_IMPROVEMENT_CARD_TYPE', function( - $q, ImprovementActionButtonObjectFactory, ThreadDataService, - UrlInterpolationService, FEEDBACK_IMPROVEMENT_CARD_TYPE) { + $q, ImprovementActionButtonObjectFactory, ImprovementModalService, + ThreadDataService, FEEDBACK_IMPROVEMENT_CARD_TYPE) { var FeedbackImprovementCard = function(feedbackThread) { - this._actionButtons = []; this._feedbackThread = feedbackThread; + this._actionButtons = [ + ImprovementActionButtonObjectFactory.createNew( + 'Review Thread', 'btn-primary', function() { + ImprovementModalService.openFeedbackThread(feedbackThread); + }), + ]; }; /** diff --git a/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactorySpec.ts b/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactorySpec.ts index ff2691121acc..a442fad34f39 100644 --- a/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactorySpec.ts +++ b/core/templates/dev/head/domain/statistics/FeedbackImprovementCardObjectFactorySpec.ts @@ -21,18 +21,23 @@ require('domain/statistics/FeedbackImprovementCardObjectFactory.ts'); describe('FeedbackImprovementCardObjectFactory', function() { var $q = null; var $rootScope = null; + var $uibModal = null; var FeedbackImprovementCardObjectFactory = null; + var ImprovementModalService = null; var ThreadDataService = null; var FEEDBACK_IMPROVEMENT_CARD_TYPE = null; beforeEach(angular.mock.module('oppia')); beforeEach(angular.mock.inject(function( - _$q_, _$rootScope_, _FeedbackImprovementCardObjectFactory_, - _ThreadDataService_, _FEEDBACK_IMPROVEMENT_CARD_TYPE_) { + _$q_, _$rootScope_, _$uibModal_, _FeedbackImprovementCardObjectFactory_, + _ImprovementModalService_, _ThreadDataService_, + _FEEDBACK_IMPROVEMENT_CARD_TYPE_) { $q = _$q_; $rootScope = _$rootScope_; + $uibModal = _$uibModal_; FeedbackImprovementCardObjectFactory = _FeedbackImprovementCardObjectFactory_; + ImprovementModalService = _ImprovementModalService_; ThreadDataService = _ThreadDataService_; FEEDBACK_IMPROVEMENT_CARD_TYPE = _FEEDBACK_IMPROVEMENT_CARD_TYPE_; })); @@ -114,8 +119,22 @@ describe('FeedbackImprovementCardObjectFactory', function() { }); describe('.getActionButtons', function() { - it('is empty', function() { - expect(this.card.getActionButtons()).toEqual([]); + it('contains one button', function() { + expect(this.card.getActionButtons().length).toEqual(1); + }); + + describe('first button', function() { + beforeEach(function() { + this.button = this.card.getActionButtons()[0]; + }); + + it('opens a thread modal', function() { + var spy = spyOn(ImprovementModalService, 'openFeedbackThread'); + + this.button.execute(); + + expect(spy).toHaveBeenCalledWith(this.mockThread); + }); }); }); }); diff --git a/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactory.ts b/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactory.ts index 4db92b8dcfb0..500f5120f054 100644 --- a/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactory.ts +++ b/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactory.ts @@ -27,10 +27,10 @@ oppia.factory('ImprovementActionButtonObjectFactory', [function() { * @param {string} [cssClass=btn-default] - The CSS class to render the button * with. */ - var ImprovementActionButton = function(text, actionFunc, cssClass) { + var ImprovementActionButton = function(text, cssClass, actionFunc) { this._text = text; + this._cssClass = cssClass; this._actionFunc = actionFunc; - this._cssClass = cssClass || 'btn-default'; }; /** @returns {string} - The text of the action (text rendered in button). */ @@ -38,16 +38,16 @@ oppia.factory('ImprovementActionButtonObjectFactory', [function() { return this._text; }; - /** Performs the associated action and return its result. */ - ImprovementActionButton.prototype.execute = function() { - return this._actionFunc(); - }; - /** Returns the CSS class of the button. */ ImprovementActionButton.prototype.getCssClass = function() { return this._cssClass; }; + /** Performs the associated action and return its result. */ + ImprovementActionButton.prototype.execute = function() { + return this._actionFunc(); + }; + return { /** * @returns {ImprovementActionButton} diff --git a/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactorySpec.ts b/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactorySpec.ts index a265f5eb2449..533a9e6b485b 100644 --- a/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactorySpec.ts +++ b/core/templates/dev/head/domain/statistics/ImprovementActionButtonObjectFactorySpec.ts @@ -19,19 +19,22 @@ require('domain/statistics/ImprovementActionButtonObjectFactory.ts'); describe('ImprovementActionButtonObjectFactory', function() { + var ImprovementActionButtonObjectFactory = null; + beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.inject(function($injector) { - this.ImprovementActionButtonObjectFactory = - $injector.get('ImprovementActionButtonObjectFactory'); + beforeEach(angular.mock.inject(function( + _ImprovementActionButtonObjectFactory_) { + ImprovementActionButtonObjectFactory = + _ImprovementActionButtonObjectFactory_; })); describe('.createNew', function() { it('stores the name and action', function() { var flagToSetOnCallback = false; - var improvementAction = - this.ImprovementActionButtonObjectFactory.createNew('Test', function() { + var improvementAction = ImprovementActionButtonObjectFactory.createNew( + 'Test', 'btn-success', function() { flagToSetOnCallback = true; - }, 'btn-success'); + }); expect(improvementAction.getText()).toEqual('Test'); expect(improvementAction.getCssClass()).toEqual('btn-success'); @@ -39,13 +42,5 @@ describe('ImprovementActionButtonObjectFactory', function() { improvementAction.execute(); expect(flagToSetOnCallback).toBe(true); }); - - it('uses btn-default as class by default', function() { - var improvementAction = - this.ImprovementActionButtonObjectFactory.createNew('Test', function() { - }); - - expect(improvementAction.getCssClass()).toEqual('btn-default'); - }); }); }); diff --git a/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactory.ts b/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactory.ts index 6961effc88aa..6aba281db759 100644 --- a/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactory.ts +++ b/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactory.ts @@ -49,8 +49,9 @@ oppia.factory('PlaythroughImprovementCardObjectFactory', [ '$scope', '$uibModalInstance', function($scope, $uibModalInstance) { $scope.confirmationMessage = - 'Are you sure you want to discard this card?'; - $scope.confirmationButtonText = 'Discard'; + 'Marking this action as resolved will discard the ' + + 'playthrough. Are you sure you want to proceed?'; + $scope.confirmationButtonText = 'Mark as Resolved'; $scope.confirmationButtonClass = 'btn-danger'; $scope.action = function() { $uibModalInstance.close(); @@ -74,7 +75,7 @@ oppia.factory('PlaythroughImprovementCardObjectFactory', [ /** @type {ImprovementActionButton[]} */ this._actionButtons = [ ImprovementActionButtonObjectFactory.createNew( - 'Discard', discardThis, 'btn-danger'), + 'Mark as Resolved', 'btn-primary', discardThis), ]; /** @type {{suggestions: string[], playthroughIds: string[]}} */ this._directiveData = { diff --git a/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactorySpec.ts b/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactorySpec.ts index 54f2b344b01f..848dbf24d360 100644 --- a/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactorySpec.ts +++ b/core/templates/dev/head/domain/statistics/PlaythroughImprovementCardObjectFactorySpec.ts @@ -20,24 +20,39 @@ require('domain/statistics/PlaythroughImprovementCardObjectFactory.ts'); require('domain/statistics/PlaythroughIssueObjectFactory.ts'); describe('PlaythroughImprovementCardObjectFactory', function() { + var $q = null; + var $rootScope = null; + var $uibModal = null; + var PlaythroughImprovementCardObjectFactory = null; + var PlaythroughIssueObjectFactory = null; + var PlaythroughIssuesService = null; + var PLAYTHROUGH_IMPROVEMENT_CARD_TYPE = null; + beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.inject(function($injector) { - this.PlaythroughImprovementCardObjectFactory = - $injector.get('PlaythroughImprovementCardObjectFactory'); - this.PlaythroughIssueObjectFactory = - $injector.get('PlaythroughIssueObjectFactory'); - this.PLAYTHROUGH_IMPROVEMENT_CARD_TYPE = - $injector.get('PLAYTHROUGH_IMPROVEMENT_CARD_TYPE'); + beforeEach(angular.mock.inject(function( + _$q_, _$rootScope_, _$uibModal_, + _PlaythroughImprovementCardObjectFactory_, + _PlaythroughIssueObjectFactory_, _PlaythroughIssuesService_, + _PLAYTHROUGH_IMPROVEMENT_CARD_TYPE_) { + $q = _$q_; + $rootScope = _$rootScope_; + $uibModal = _$uibModal_; + PlaythroughImprovementCardObjectFactory = + _PlaythroughImprovementCardObjectFactory_; + PlaythroughIssueObjectFactory = _PlaythroughIssueObjectFactory_; + PlaythroughIssuesService = _PlaythroughIssuesService_; + PLAYTHROUGH_IMPROVEMENT_CARD_TYPE = _PLAYTHROUGH_IMPROVEMENT_CARD_TYPE_; + + PlaythroughIssuesService.initSession(expId, expVersion); var expId = '7'; var expVersion = 1; - this.PlaythroughIssuesService = $injector.get('PlaythroughIssuesService'); - this.PlaythroughIssuesService.initSession(expId, expVersion); + this.scope = $rootScope.$new(); })); describe('.createNew', function() { it('retrieves data from passed issue', function() { - var issue = this.PlaythroughIssueObjectFactory.createFromBackendDict({ + var issue = PlaythroughIssueObjectFactory.createFromBackendDict({ issue_type: 'EarlyQuit', issue_customization_args: { state_name: {value: 'Hola'}, @@ -48,25 +63,25 @@ describe('PlaythroughImprovementCardObjectFactory', function() { is_valid: true, }); - var card = this.PlaythroughImprovementCardObjectFactory.createNew(issue); + var card = PlaythroughImprovementCardObjectFactory.createNew(issue); expect(card.getTitle()).toEqual( - this.PlaythroughIssuesService.renderIssueStatement(issue)); + PlaythroughIssuesService.renderIssueStatement(issue)); expect(card.getDirectiveData()).toEqual({ - title: this.PlaythroughIssuesService.renderIssueStatement(issue), + title: PlaythroughIssuesService.renderIssueStatement(issue), suggestions: - this.PlaythroughIssuesService.renderIssueSuggestions(issue), + PlaythroughIssuesService.renderIssueSuggestions(issue), playthroughIds: ['1', '2'], }); expect(card.getDirectiveType()).toEqual( - this.PLAYTHROUGH_IMPROVEMENT_CARD_TYPE); + PLAYTHROUGH_IMPROVEMENT_CARD_TYPE); }); }); describe('.fetchCards', function() { it('returns a card for each existing issue', function(done) { var earlyQuitIssue = - this.PlaythroughIssueObjectFactory.createFromBackendDict({ + PlaythroughIssueObjectFactory.createFromBackendDict({ issue_type: 'EarlyQuit', issue_customization_args: { state_name: {value: 'Hola'}, @@ -77,10 +92,10 @@ describe('PlaythroughImprovementCardObjectFactory', function() { is_valid: true, }); var earlyQuitCardTitle = - this.PlaythroughIssuesService.renderIssueStatement(earlyQuitIssue); + PlaythroughIssuesService.renderIssueStatement(earlyQuitIssue); var multipleIncorrectSubmissionsIssue = - this.PlaythroughIssueObjectFactory.createFromBackendDict({ + PlaythroughIssueObjectFactory.createFromBackendDict({ issue_type: 'MultipleIncorrectSubmissions', issue_customization_args: { state_name: {value: 'Hola'}, @@ -91,11 +106,11 @@ describe('PlaythroughImprovementCardObjectFactory', function() { is_valid: true, }); var multipleIncorrectSubmissionsCardTitle = - this.PlaythroughIssuesService.renderIssueStatement( + PlaythroughIssuesService.renderIssueStatement( multipleIncorrectSubmissionsIssue); var cyclicTransitionsIssue = - this.PlaythroughIssueObjectFactory.createFromBackendDict({ + PlaythroughIssueObjectFactory.createFromBackendDict({ issue_type: 'CyclicTransitions', issue_customization_args: { state_names: {value: ['Hola', 'Me Llamo', 'Hola']}, @@ -105,17 +120,17 @@ describe('PlaythroughImprovementCardObjectFactory', function() { is_valid: true, }); var cyclicTransitionsCardTitle = - this.PlaythroughIssuesService.renderIssueStatement( + PlaythroughIssuesService.renderIssueStatement( cyclicTransitionsIssue); - spyOn(this.PlaythroughIssuesService, 'getIssues').and.returnValue( - Promise.resolve([ + spyOn(PlaythroughIssuesService, 'getIssues').and.returnValue( + $q.resolve([ earlyQuitIssue, multipleIncorrectSubmissionsIssue, cyclicTransitionsIssue, ])); - this.PlaythroughImprovementCardObjectFactory.fetchCards() + PlaythroughImprovementCardObjectFactory.fetchCards() .then(function(cards) { expect(cards.length).toEqual(3); expect(cards[0].getTitle()).toEqual(earlyQuitCardTitle); @@ -123,12 +138,14 @@ describe('PlaythroughImprovementCardObjectFactory', function() { .toEqual(multipleIncorrectSubmissionsCardTitle); expect(cards[2].getTitle()).toEqual(cyclicTransitionsCardTitle); }).then(done, done.fail); + + this.scope.$digest(); // Forces all pending promises to evaluate. }); }); describe('PlaythroughImprovementCard', function() { beforeEach(function() { - this.issue = this.PlaythroughIssueObjectFactory.createFromBackendDict({ + this.issue = PlaythroughIssueObjectFactory.createFromBackendDict({ issue_type: 'EarlyQuit', issue_customization_args: { state_name: {value: 'Hola'}, @@ -138,62 +155,62 @@ describe('PlaythroughImprovementCardObjectFactory', function() { schema_version: 1, is_valid: true, }); - this.card = - this.PlaythroughImprovementCardObjectFactory.createNew(this.issue); + this.card = PlaythroughImprovementCardObjectFactory.createNew(this.issue); }); describe('.getActionButtons', function() { it('contains a specific sequence of buttons', function() { expect(this.card.getActionButtons().length).toEqual(1); - expect(this.card.getActionButtons()[0].getText()).toEqual('Discard'); + expect(this.card.getActionButtons()[0].getText()) + .toEqual('Mark as Resolved'); }); }); - describe('Discard Action Button', function() { - beforeEach(angular.mock.inject(function($injector) { - this.$uibModal = $injector.get('$uibModal'); - })); - + describe('Mark as Resolved Action Button', function() { it('marks the card as resolved after confirmation', function(done) { var card = this.card; var issue = this.issue; - var discardActionButton = card.getActionButtons()[0]; + var resolveActionButton = card.getActionButtons()[0]; var resolveIssueSpy = - spyOn(this.PlaythroughIssuesService, 'resolveIssue').and.stub(); + spyOn(PlaythroughIssuesService, 'resolveIssue').and.stub(); - spyOn(this.$uibModal, 'open').and.returnValue({ - result: Promise.resolve(), // Returned when confirm button is pressed. + spyOn($uibModal, 'open').and.returnValue({ + result: $q.resolve(), // Returned when confirm button is pressed. }); expect(card.isOpen()).toBe(true); - discardActionButton.execute().then(function() { + resolveActionButton.execute().then(function() { expect(resolveIssueSpy).toHaveBeenCalledWith(issue); expect(card.isOpen()).toBe(false); done(); }, function() { done.fail('dismiss button unexpectedly failed.'); }); + + this.scope.$digest(); // Forces all pending promises to evaluate. }); it('keeps the card after cancel', function(done) { var card = this.card; var issue = this.issue; - var discardActionButton = card.getActionButtons()[0]; + var resolveActionButton = card.getActionButtons()[0]; var resolveIssueSpy = - spyOn(this.PlaythroughIssuesService, 'resolveIssue').and.stub(); + spyOn(PlaythroughIssuesService, 'resolveIssue').and.stub(); - spyOn(this.$uibModal, 'open').and.returnValue({ - result: Promise.reject(), // Returned when cancel button is pressed. + spyOn($uibModal, 'open').and.returnValue({ + result: $q.reject(), // Returned when cancel button is pressed. }); expect(card.isOpen()).toBe(true); - discardActionButton.execute().then(function() { + resolveActionButton.execute().then(function() { done.fail('dismiss button unexpectedly succeeded.'); }, function() { expect(resolveIssueSpy).not.toHaveBeenCalled(); expect(card.isOpen()).toBe(true); done(); }); + + this.scope.$digest(); // Forces all pending promises to evaluate. }); }); }); diff --git a/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactory.ts b/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactory.ts index 9e847590efbd..b1f4cd2a05f8 100644 --- a/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactory.ts +++ b/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactory.ts @@ -16,24 +16,32 @@ * @fileoverview Factory for creating Suggestion Cards in the Improvements Tab. */ - require('domain/statistics/ImprovementActionButtonObjectFactory.ts'); -require('domain/utilities/UrlInterpolationService.ts'); - +require( + 'pages/exploration-editor-page/improvements-tab/services/' + + 'improvement-modal.service.ts'); require( 'pages/exploration-editor-page/feedback-tab/services/thread-data.service.ts'); +require( + 'pages/exploration-editor-page/suggestion-modal-for-editor-view/' + + 'suggestion-modal-for-exploration-editor.service.ts'); require('domain/statistics/statistics-domain.constants.ts'); var oppia = require('AppInit.ts').module; oppia.factory('SuggestionImprovementCardObjectFactory', [ - '$q', 'ImprovementActionButtonObjectFactory', 'ThreadDataService', - 'UrlInterpolationService', 'SUGGESTION_IMPROVEMENT_CARD_TYPE', + '$q', 'ImprovementActionButtonObjectFactory', 'ImprovementModalService', + 'ThreadDataService', 'SUGGESTION_IMPROVEMENT_CARD_TYPE', function( - $q, ImprovementActionButtonObjectFactory, ThreadDataService, - UrlInterpolationService, SUGGESTION_IMPROVEMENT_CARD_TYPE) { + $q, ImprovementActionButtonObjectFactory, ImprovementModalService, + ThreadDataService, SUGGESTION_IMPROVEMENT_CARD_TYPE) { var SuggestionImprovementCard = function(suggestionThread) { - this._actionButtons = []; + this._actionButtons = [ + ImprovementActionButtonObjectFactory.createNew( + 'Review Thread', 'btn-primary', function() { + ImprovementModalService.openSuggestionThread(suggestionThread); + }), + ]; this._suggestionThread = suggestionThread; }; diff --git a/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactorySpec.ts b/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactorySpec.ts index de5c69526b0d..6926e3b1e480 100644 --- a/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactorySpec.ts +++ b/core/templates/dev/head/domain/statistics/SuggestionImprovementCardObjectFactorySpec.ts @@ -21,18 +21,30 @@ require('domain/statistics/SuggestionImprovementCardObjectFactory.ts'); describe('SuggestionImprovementCardObjectFactory', function() { var $q = null; var $rootScope = null; + var $uibModal = null; + var ImprovementModalService = null; var SuggestionImprovementCardObjectFactory = null; + var SuggestionModalForExplorationEditorService = null; + var SuggestionThreadObjectFactory = null; var ThreadDataService = null; var SUGGESTION_IMPROVEMENT_CARD_TYPE = null; beforeEach(angular.mock.module('oppia')); beforeEach(angular.mock.inject(function( - _$q_, _$rootScope_, _SuggestionImprovementCardObjectFactory_, - _ThreadDataService_, _SUGGESTION_IMPROVEMENT_CARD_TYPE_) { + _$q_, _$rootScope_, _$uibModal_, _ImprovementModalService_, + _SuggestionImprovementCardObjectFactory_, + _SuggestionModalForExplorationEditorService_, + _SuggestionThreadObjectFactory_, _ThreadDataService_, + _SUGGESTION_IMPROVEMENT_CARD_TYPE_) { $q = _$q_; $rootScope = _$rootScope_; + $uibModal = _$uibModal_; + ImprovementModalService = _ImprovementModalService_; SuggestionImprovementCardObjectFactory = _SuggestionImprovementCardObjectFactory_; + SuggestionModalForExplorationEditorService = + _SuggestionModalForExplorationEditorService_; + SuggestionThreadObjectFactory = _SuggestionThreadObjectFactory_; ThreadDataService = _ThreadDataService_; SUGGESTION_IMPROVEMENT_CARD_TYPE = _SUGGESTION_IMPROVEMENT_CARD_TYPE_; })); @@ -68,15 +80,40 @@ describe('SuggestionImprovementCardObjectFactory', function() { describe('SuggestionImprovementCard', function() { beforeEach(function() { - this.mockThread = { - last_updated: 1441870501230.642, - original_author_username: 'test_learner', - state_name: null, - status: 'open', - subject: 'Suggestion from a learner', - summary: null, - thread_id: 'abc1', + var mockSuggestionThreadBackendDict = { + last_updated: 1000, + original_author_username: 'author', + status: 'accepted', + subject: 'sample subject', + summary: 'sample summary', + message_count: 10, + state_name: 'state 1', + thread_id: 'exploration.exp1.thread1' }; + var mockSuggestionBackendDict = { + suggestion_id: 'exploration.exp1.thread1', + suggestion_type: 'edit_exploration_state_content', + target_type: 'exploration', + target_id: 'exp1', + target_version_at_submission: 1, + status: 'accepted', + author_name: 'author', + change: { + cmd: 'edit_state_property', + property_name: 'content', + state_name: 'state_1', + new_value: { + html: 'new suggestion content' + }, + old_value: { + html: 'old suggestion content' + } + }, + last_updated: 1000 + }; + + this.mockThread = SuggestionThreadObjectFactory.createFromBackendDicts( + mockSuggestionThreadBackendDict, mockSuggestionBackendDict); this.card = SuggestionImprovementCardObjectFactory.createNew(this.mockThread); }); @@ -114,8 +151,22 @@ describe('SuggestionImprovementCardObjectFactory', function() { }); describe('.getActionButtons', function() { - it('is empty', function() { - expect(this.card.getActionButtons()).toEqual([]); + it('contains one button', function() { + expect(this.card.getActionButtons().length).toEqual(1); + }); + + describe('first button', function() { + beforeEach(function() { + this.button = this.card.getActionButtons()[0]; + }); + + it('opens a thread modal', function() { + var spy = spyOn(ImprovementModalService, 'openSuggestionThread'); + + this.button.execute(); + + expect(spy).toHaveBeenCalledWith(this.mockThread); + }); }); }); }); diff --git a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/feedback-improvement-card/feedback-improvement-card.directive.html b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/feedback-improvement-card/feedback-improvement-card.directive.html index e15c47ab4f84..9565a854441f 100644 --- a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/feedback-improvement-card/feedback-improvement-card.directive.html +++ b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/feedback-improvement-card/feedback-improvement-card.directive.html @@ -1,7 +1,7 @@
-

+

<[getData().subject]> -

+
<[getHumanReadableStatus()]>
@@ -14,7 +14,7 @@

- anonymously submitted + (anonymously submitted) by <[getLatestMessage().author]> diff --git a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/improvements-tab.directive.html b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/improvements-tab.directive.html index ee7f234e7b70..650639e31663 100644 --- a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/improvements-tab.directive.html +++ b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/improvements-tab.directive.html @@ -1,5 +1,5 @@
-
+

Open Cards

@@ -7,27 +7,21 @@

<[getOpenCardCount()]>

-
- -
- - - - - - -
-
- -
-
-
+ + + +
diff --git a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/services/improvement-modal.service.ts b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/services/improvement-modal.service.ts new file mode 100644 index 000000000000..55bc8f3f8c25 --- /dev/null +++ b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/services/improvement-modal.service.ts @@ -0,0 +1,199 @@ +// Copyright 2019 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Service for creating modals associated to the improvements tab. + */ + +require( + 'pages/exploration-editor-page/feedback-tab/services/thread-data.service.ts'); +require( + 'pages/exploration-editor-page/feedback-tab/services/' + + 'thread-status-display.service.ts'); +require( + 'pages/exploration-editor-page/suggestion-modal-for-editor-view/' + + 'suggestion-modal-for-exploration-editor.service.ts'); +require('domain/utilities/UrlInterpolationService.ts'); + +var oppia = require('AppInit.ts').module; + +oppia.factory('ImprovementModalService', [ + '$uibModal', 'AlertsService', 'ChangeListService', 'DateTimeFormatService', + 'EditabilityService', 'ExplorationStatesService', + 'SuggestionModalForExplorationEditorService', 'ThreadDataService', + 'ThreadStatusDisplayService', 'UrlInterpolationService', 'UserService', + function( + $uibModal, AlertsService, ChangeListService, DateTimeFormatService, + EditabilityService, ExplorationStatesService, + SuggestionModalForExplorationEditorService, ThreadDataService, + ThreadStatusDisplayService, UrlInterpolationService, UserService) { + return { + openFeedbackThread: function(thread) { + return $uibModal.open({ + templateUrl: UrlInterpolationService.getDirectiveTemplateUrl( + '/pages/exploration-editor-page/improvements-tab/templates/' + + 'feedback-thread-modal.template.html'), + resolve: { + isUserLoggedIn: function() { + return UserService.getUserInfoAsync().then(function(userInfo) { + return userInfo.isLoggedIn(); + }); + }, + }, + controller: [ + '$scope', '$uibModalInstance', 'isUserLoggedIn', + function($scope, $uibModalInstance, isUserLoggedIn) { + $scope.activeThread = thread; + $scope.isUserLoggedIn = isUserLoggedIn; + $scope.STATUS_CHOICES = ThreadStatusDisplayService.STATUS_CHOICES; + $scope.getLabelClass = ThreadStatusDisplayService.getLabelClass; + $scope.getHumanReadableStatus = ( + ThreadStatusDisplayService.getHumanReadableStatus); + $scope.getLocaleAbbreviatedDatetimeString = ( + DateTimeFormatService.getLocaleAbbreviatedDatetimeString); + $scope.EditabilityService = EditabilityService; + + // Initial load of the thread list on page load. + $scope.tmpMessage = { + status: $scope.activeThread.status, + text: '', + }; + + $scope.getTitle = function() { + return $scope.activeThread.subject; + }; + + // TODO(Allan): Implement ability to edit suggestions before + // applying. + $scope.addNewMessage = function(threadId, tmpText, tmpStatus) { + if (threadId === null) { + AlertsService.addWarning( + 'Cannot add message to thread with ID: null.'); + return; + } + if (!tmpStatus) { + AlertsService.addWarning( + 'Invalid message status: ' + tmpStatus); + return; + } + $scope.messageSendingInProgress = true; + ThreadDataService.addNewMessage( + threadId, tmpText, tmpStatus, function() { + $scope.tmpMessage.status = $scope.activeThread.status; + $scope.messageSendingInProgress = false; + }, function() { + $scope.messageSendingInProgress = false; + }); + }; + $scope.close = function() { + $uibModalInstance.close(); + }; + }, + ], + backdrop: 'static', + size: 'lg', + }); + }, + + openSuggestionThread: function(thread) { + var openSuggestionReviewer = this.openSuggestionReviewer; + return $uibModal.open({ + templateUrl: UrlInterpolationService.getDirectiveTemplateUrl( + '/pages/exploration-editor-page/improvements-tab/templates/' + + 'suggestion-thread-modal.template.html'), + resolve: { + isUserLoggedIn: function() { + return UserService.getUserInfoAsync().then(function(userInfo) { + return userInfo.isLoggedIn(); + }); + }, + }, + controller: [ + '$scope', '$uibModalInstance', 'isUserLoggedIn', + function($scope, $uibModalInstance, isUserLoggedIn) { + $scope.activeThread = thread; + $scope.isUserLoggedIn = isUserLoggedIn; + $scope.STATUS_CHOICES = ThreadStatusDisplayService.STATUS_CHOICES; + $scope.getLabelClass = ThreadStatusDisplayService.getLabelClass; + $scope.getHumanReadableStatus = ( + ThreadStatusDisplayService.getHumanReadableStatus); + $scope.getLocaleAbbreviatedDatetimeString = ( + DateTimeFormatService.getLocaleAbbreviatedDatetimeString); + $scope.EditabilityService = EditabilityService; + $scope.openSuggestionReviewer = openSuggestionReviewer; + + // Initial load of the thread list on page load. + $scope.tmpMessage = { + status: $scope.activeThread.status, + text: '', + }; + + $scope.getTitle = function() { + return ( + 'Suggestion for the card "' + + $scope.activeThread.suggestion.stateName + '"'); + }; + + // TODO(Allan): Implement ability to edit suggestions before + // applying. + $scope.addNewMessage = function(threadId, tmpText, tmpStatus) { + if (threadId === null) { + AlertsService.addWarning( + 'Cannot add message to thread with ID: null.'); + return; + } + if (!tmpStatus) { + AlertsService.addWarning( + 'Invalid message status: ' + tmpStatus); + return; + } + $scope.messageSendingInProgress = true; + ThreadDataService.addNewMessage( + threadId, tmpText, tmpStatus, function() { + $scope.tmpMessage.status = $scope.activeThread.status; + $scope.messageSendingInProgress = false; + }, function() { + $scope.messageSendingInProgress = false; + }); + }; + $scope.close = function() { + $uibModalInstance.close(); + }; + }, + ], + backdrop: 'static', + size: 'lg', + }); + }, + + openSuggestionReviewer: function(suggestionThread) { + return SuggestionModalForExplorationEditorService.showSuggestionModal( + suggestionThread.suggestion.suggestionType, { + activeThread: suggestionThread, + hasUnsavedChanges: function() { + return ChangeListService.getChangeList().length > 0; + }, + isSuggestionHandled: function() { + return suggestionThread.isSuggestionHandled(); + }, + isSuggestionValid: function() { + return ExplorationStatesService.hasState( + suggestionThread.getSuggestionStateName()); + }, + } + ); + }, + }; + }, +]); diff --git a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/suggestion-improvement-card/suggestion-improvement-card.directive.html b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/suggestion-improvement-card/suggestion-improvement-card.directive.html index d5b042fedc61..2e4776c54d24 100644 --- a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/suggestion-improvement-card/suggestion-improvement-card.directive.html +++ b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/suggestion-improvement-card/suggestion-improvement-card.directive.html @@ -1,7 +1,7 @@
-

+

Suggestion for the card "<[getData().suggestion.stateName]>" -

+
<[getHumanReadableStatus()]>
@@ -14,7 +14,7 @@

- anonymously submitted + (anonymously submitted) by <[getLatestMessage().author]> diff --git a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/templates/feedback-thread-modal.template.html b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/templates/feedback-thread-modal.template.html new file mode 100644 index 000000000000..bfa601d31e05 --- /dev/null +++ b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/templates/feedback-thread-modal.template.html @@ -0,0 +1,118 @@ + + + + + + + + diff --git a/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/templates/suggestion-thread-modal.template.html b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/templates/suggestion-thread-modal.template.html new file mode 100644 index 000000000000..e00e71cd23f3 --- /dev/null +++ b/core/templates/dev/head/pages/exploration-editor-page/improvements-tab/templates/suggestion-thread-modal.template.html @@ -0,0 +1,105 @@ + + + + + + + + diff --git a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/issues/answer-submit-action.directive.html b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/issues/answer-submit-action.directive.html new file mode 100644 index 000000000000..f8a26b7df177 --- /dev/null +++ b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/issues/answer-submit-action.directive.html @@ -0,0 +1,9 @@ +<[$ctrl.actionIndex]>. Submitted answer " + + + " in card "<[$ctrl.currentStateName]>". + + + " and moved to card "<[$ctrl.destStateName]>" after spending <[$ctrl.timeSpentInStateSecs]> + seconds on card "<[$ctrl.currentStateName]>". + diff --git a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/issues/answer-submit-action.directive.ts b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/issues/answer-submit-action.directive.ts new file mode 100644 index 000000000000..a0d475d21c0e --- /dev/null +++ b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/issues/answer-submit-action.directive.ts @@ -0,0 +1,56 @@ +// Copyright 2019 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Directive for the Answer Submit Learner Action. + */ + +require('domain/utilities/UrlInterpolationService.ts'); +require('services/ExplorationHtmlFormatterService.ts'); +require('services/HtmlEscaperService.ts'); + +var oppia = require('AppInit.ts').module; + +oppia.directive('answerSubmitAction', [ + 'ExplorationHtmlFormatterService', 'HtmlEscaperService', + 'UrlInterpolationService', + function( + ExplorationHtmlFormatterService, HtmlEscaperService, + UrlInterpolationService) { + return { + restrict: 'E', + scope: {}, + bindToController: {}, + templateUrl: UrlInterpolationService.getDirectiveTemplateUrl( + '/pages/exploration-editor-page/statistics-tab/issues/' + + 'answer-submit-action.directive.html'), + controllerAs: '$ctrl', + controller: ['$attrs', function($attrs) { + var ctrl = this; + ctrl.currentStateName = $attrs.currentStateName; + ctrl.destStateName = $attrs.destStateName; + ctrl.actionIndex = $attrs.actionIndex; + ctrl.timeSpentInStateSecs = $attrs.timeSpentInStateSecs; + + var _customizationArgs = HtmlEscaperService.escapedJsonToObj( + $attrs.interactionCustomizationArgs); + var _answer = HtmlEscaperService.escapedJsonToObj($attrs.answer); + ctrl.getShortAnswerHtml = function() { + return ExplorationHtmlFormatterService.getShortAnswerHtml( + _answer, $attrs.interactionId, _customizationArgs); + }; + }] + }; + } +]); diff --git a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.spec.ts b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.spec.ts index 9abf1153b52a..9c97e2f2d45a 100644 --- a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.spec.ts +++ b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.spec.ts @@ -41,12 +41,32 @@ describe('Learner Action Render Service', function() { this.LearnerActionObjectFactory = $injector.get('LearnerActionObjectFactory'); this.PlaythroughService = $injector.get('PlaythroughService'); + this.ExplorationStatesService = $injector.get('ExplorationStatesService'); this.ExplorationFeaturesService = $injector.get('ExplorationFeaturesService'); this.PlaythroughService.initSession('expId1', 1, 1.0); spyOn(this.ExplorationFeaturesService, 'isPlaythroughRecordingEnabled') .and.returnValue(true); + spyOn(this.ExplorationStatesService, 'getState') + .withArgs('stateName1').and.returnValue( + { interaction: { id: 'Continue'}}) + .withArgs('stateName2').and.returnValue( + { interaction: { id: 'TextInput'}}) + .withArgs('stateName3').and.returnValue( + { interaction: { + id: 'MultipleChoiceInput', + customizationArgs: { + choices: { + value: [ + 'Choice1', + 'Choice2', + 'Choice3' + ] + } + }} + }); + this.LearnerActionRenderService = $injector.get('LearnerActionRenderService'); })); @@ -311,8 +331,7 @@ describe('Learner Action Render Service', function() { .renderFinalDisplayBlockForMISIssueHTML(displayBlocks[0], 1); expect(this.$sce.getTrustedHtml(finalBlockHTML)).toEqual( - '1. Started exploration ' + - 'at card "stateName1".' + + '1. Started exploration at card "stateName1".' + '2. Submitted the ' + 'following answers in card "stateName1"' + '' + '' + '
Answer' + @@ -322,8 +341,8 @@ describe('Learner Action Render Service', function() { '
HelloTry again
HelloTry again
HelloTry again
' + - '3. Left the exploration ' + - 'after spending a total of 120 seconds on card "stateName1".' + '3. Left the exploration after spending a total of 120 seconds on ' + + 'card "stateName1".' ); }); @@ -332,7 +351,10 @@ describe('Learner Action Render Service', function() { this.PlaythroughService.recordAnswerSubmitAction( 'stateName1', 'stateName2', 'Continue', '', 'Welcome', 30); this.PlaythroughService.recordAnswerSubmitAction( - 'stateName2', 'stateName2', 'TextInput', 'Hello', 'Try again', 30); + 'stateName2', 'stateName3', 'TextInput', 'Hello', 'Go ahead', 30); + this.PlaythroughService.recordAnswerSubmitAction( + 'stateName3', 'stateName3', 'MultipleChoiceInput', 'Choice1', + 'Go ahead', 30); this.PlaythroughService.recordExplorationQuitAction('stateName2', 120); var learnerActions = this.PlaythroughService.getPlaythrough().actions; @@ -341,19 +363,33 @@ describe('Learner Action Render Service', function() { expect(displayBlocks.length).toEqual(1); - var blockHTML = this.LearnerActionRenderService.renderDisplayBlockHTML( - displayBlocks[0], 1); + var actionHtmlList = []; + for (var i = 0; i < displayBlocks[0].length; i++) { + actionHtmlList.push(this.LearnerActionRenderService.renderLearnerAction( + displayBlocks[0][i], 0, i + 1)); + } - expect(this.$sce.getTrustedHtml(blockHTML)).toEqual( - '1. Started exploration at ' + - 'card "stateName1".' + - '2. Pressed "Continue" to ' + - 'move to card "stateName2" after 30 seconds.' + - '3. Submitted answer ' + - '"Hello" in card "stateName2".' + - '4. Left the exploration ' + - 'after spending a total of 120 seconds on card "stateName2".' - ); + expect(actionHtmlList[0]).toEqual( + '1. Started exploration at card "stateName1".'); + expect(actionHtmlList[1]).toEqual( + '2. Pressed "Continue" to move to card "stateName2" after 30 seconds.'); + expect(actionHtmlList[2]).toEqual( + ''); + expect(actionHtmlList[3]).toEqual( + ''); + expect(actionHtmlList[4]).toEqual( + '5. Left the exploration after spending a total of 120 seconds on ' + + 'card "stateName2".'); }); }); }); diff --git a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.ts b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.ts index ff96eaf1a0d7..1b1d2788fc64 100644 --- a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.ts +++ b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/services/learner-action-render.service.ts @@ -26,38 +26,44 @@ * learner actions and then returns a giant HTML string. */ +require('pages/exploration-editor-page/services/exploration-states.service.ts'); +require( + 'pages/exploration-editor-page/statistics-tab/issues/' + + 'answer-submit-action.directive.ts'); +require('services/ExplorationHtmlFormatterService.ts'); + var oppia = require('AppInit.ts').module; oppia.factory('LearnerActionRenderService', [ - '$sce', 'ACTION_TYPE_ANSWER_SUBMIT', 'ACTION_TYPE_EXPLORATION_QUIT', - 'ACTION_TYPE_EXPLORATION_START', 'ISSUE_TYPE_MULTIPLE_INCORRECT_SUBMISSIONS', + '$sce', 'ExplorationHtmlFormatterService', 'ExplorationStatesService', + 'HtmlEscaperService', 'ACTION_TYPE_ANSWER_SUBMIT', + 'ACTION_TYPE_EXPLORATION_QUIT', 'ACTION_TYPE_EXPLORATION_START', + 'ISSUE_TYPE_MULTIPLE_INCORRECT_SUBMISSIONS', function( - $sce, ACTION_TYPE_ANSWER_SUBMIT, ACTION_TYPE_EXPLORATION_QUIT, - ACTION_TYPE_EXPLORATION_START, + $sce, ExplorationHtmlFormatterService, ExplorationStatesService, + HtmlEscaperService, ACTION_TYPE_ANSWER_SUBMIT, + ACTION_TYPE_EXPLORATION_QUIT, ACTION_TYPE_EXPLORATION_START, ISSUE_TYPE_MULTIPLE_INCORRECT_SUBMISSIONS) { var renderExplorationStartActionHTML = function(stateName, actionIndex) { - var htmlString = - '' + actionIndex + - '. Started exploration at card "' + stateName + '".'; - return htmlString; + var statement = + actionIndex + '. Started exploration at card "' + stateName + '".'; + return ($('').text(statement)).html(); }; var renderExplorationQuitActionHTML = function( stateName, timeSpentInStateSecs, actionIndex) { - var htmlString = - '' + actionIndex + - '. Left the exploration after spending a ' + 'total of ' + - timeSpentInStateSecs + ' seconds on card "' + stateName + '".'; - return htmlString; + var statement = + actionIndex + '. Left the exploration after spending a total of ' + + timeSpentInStateSecs + ' seconds on card "' + stateName + '".'; + return ($('').text(statement)).html(); }; var renderContinueButtonSubmitActionHTML = function( stateName, timeSpentInStateSecs, actionIndex) { - var htmlString = - '' + actionIndex + - '. Pressed "Continue" to move to card "' + stateName + '" after ' + - timeSpentInStateSecs + ' seconds.'; - return htmlString; + var statement = + actionIndex + '. Pressed "Continue" to move to card "' + stateName + + '" after ' + timeSpentInStateSecs + ' seconds.'; + return ($('').text(statement)).html(); }; /** @@ -68,25 +74,23 @@ oppia.factory('LearnerActionRenderService', [ * @param {int} timeSpentInStateSecs. * @param {string} currentStateName. * @param {int} actionIndex. + * @param {Interaction} interaction. * @returns {string} */ var renderAnswerSubmitActionHTML = function( answer, destStateName, timeSpentInStateSecs, currentStateName, - actionIndex) { - var htmlString; - if (currentStateName === destStateName) { - htmlString = - '' + actionIndex + - '. Submitted answer "' + answer + '" in card "' + currentStateName + - '".'; - } else { - htmlString = - '' + actionIndex + - '. Submitted answer "' + answer + '" and moved to card "' + - destStateName + '" after spending ' + timeSpentInStateSecs + - ' seconds on card "' + currentStateName + '".'; - } - return htmlString; + actionIndex, interaction) { + var el = $(''); + el.attr('answer', HtmlEscaperService.objToEscapedJson(answer)); + el.attr('dest-state-name', destStateName); + el.attr('time-spent-in-state-secs', timeSpentInStateSecs); + el.attr('current-state-name', currentStateName); + el.attr('action-index', actionIndex); + el.attr('interaction-id', interaction.id); + el.attr( + 'interaction-customization-args', + HtmlEscaperService.objToEscapedJson(interaction.customizationArgs)); + return ($('').append(el)).html(); }; /** @@ -129,6 +133,8 @@ oppia.factory('LearnerActionRenderService', [ var renderLearnerActionHTML = function(learnerAction, actionIndex) { var actionType = learnerAction.actionType; var custArgs = learnerAction.actionCustomizationArgs; + var interaction = ExplorationStatesService.getState( + custArgs.state_name.value).interaction; if (actionType === ACTION_TYPE_EXPLORATION_START) { return renderExplorationStartActionHTML( custArgs.state_name.value, actionIndex); @@ -146,7 +152,7 @@ oppia.factory('LearnerActionRenderService', [ return renderAnswerSubmitActionHTML( custArgs.submitted_answer.value, custArgs.dest_state_name.value, custArgs.time_spent_state_in_msecs.value, custArgs.state_name.value, - actionIndex); + actionIndex, interaction); } } }; @@ -217,6 +223,10 @@ oppia.factory('LearnerActionRenderService', [ block[index], actionStartIndex + i + 1); return $sce.trustAsHtml(htmlString); }, + renderLearnerAction: function(learnerAction, blockIndex, actionIndex) { + return renderLearnerActionHTML( + learnerAction, blockIndex + actionIndex); + }, renderDisplayBlockHTML: function(block, actionStartIndex) { var htmlString = ''; for (var i = 0; i < block.length; i++) { diff --git a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/templates/playthrough-modal.template.html b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/templates/playthrough-modal.template.html index b747b7fac694..892fbd789dbd 100644 --- a/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/templates/playthrough-modal.template.html +++ b/core/templates/dev/head/pages/exploration-editor-page/statistics-tab/templates/playthrough-modal.template.html @@ -8,9 +8,19 @@
- + +
+
+
+ + +
-
diff --git a/core/templates/dev/head/pages/topic-editor-page/modal-templates/select-skill-modal.template.html b/core/templates/dev/head/pages/topic-editor-page/modal-templates/select-skill-modal.template.html index 72e2fee6c690..0d096361de8a 100644 --- a/core/templates/dev/head/pages/topic-editor-page/modal-templates/select-skill-modal.template.html +++ b/core/templates/dev/head/pages/topic-editor-page/modal-templates/select-skill-modal.template.html @@ -1,31 +1,75 @@ - -