core/domain/suggestion_registry_test.py
# 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.
"""Tests for suggestion registry classes."""
from __future__ import annotations
import datetime
import os
from core import feconf
from core import utils
from core.constants import constants
from core.domain import change_domain
from core.domain import exp_domain
from core.domain import exp_services
from core.domain import fs_services
from core.domain import html_validation_service
from core.domain import platform_parameter_services
from core.domain import question_domain
from core.domain import question_services
from core.domain import skill_services
from core.domain import state_domain
from core.domain import suggestion_registry
from core.domain import suggestion_services
from core.domain import topic_fetchers
from core.domain import translation_domain
from core.domain import translation_fetchers
from core.domain import user_services
from core.platform import models
from core.tests import test_utils
from extensions import domain
from typing import Dict, Final, List, Optional, TypedDict, Union, cast
MYPY = False
if MYPY: # pragma: no cover
from mypy_imports import opportunity_models
from mypy_imports import suggestion_models
(
suggestion_models, opportunity_models
) = models.Registry.import_models([
models.Names.SUGGESTION, models.Names.OPPORTUNITY
])
ChangeType = Dict[
str, Union[str, float, Dict[str, Union[str, int, state_domain.StateDict]]]
]
class MockInvalidSuggestion(suggestion_registry.BaseSuggestion):
def __init__(self) -> None: # pylint: disable=super-init-not-called
pass
class BaseSuggestionUnitTests(test_utils.GenericTestBase):
"""Tests for the BaseSuggestion class."""
def setUp(self) -> None:
super().setUp()
self.base_suggestion = MockInvalidSuggestion()
def test_base_class_accept_raises_error(self) -> None:
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement accept.'):
self.base_suggestion.accept('test_message')
def test_base_class_pre_accept_validate_raises_error(self) -> None:
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement'
' pre_accept_validate.'):
self.base_suggestion.pre_accept_validate()
def test_base_class_populate_old_value_of_change_raises_error(self) -> None:
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement'
' populate_old_value_of_change.'):
self.base_suggestion.populate_old_value_of_change()
def test_base_class_pre_update_validate_raises_error(self) -> None:
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement'
' pre_update_validate.'):
self.base_suggestion.pre_update_validate({})
def test_base_class_get_all_html_content_strings(self) -> None:
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement'
' get_all_html_content_strings.'):
self.base_suggestion.get_all_html_content_strings()
def test_base_class_get_target_entity_html_strings(self) -> None:
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement'
' get_target_entity_html_strings.'):
self.base_suggestion.get_target_entity_html_strings()
def test_base_class_convert_html_in_suggestion_change(self) -> None:
def conversion_fn(_: str) -> str:
"""Temporary function."""
return 'abcd'
with self.assertRaisesRegex(
NotImplementedError,
'Subclasses of BaseSuggestion should implement'
' convert_html_in_suggestion_change.'):
self.base_suggestion.convert_html_in_suggestion_change(
conversion_fn)
class SuggestionEditStateContentDict(TypedDict):
"""Dictionary representing the SuggestionEditStateContent object."""
suggestion_id: str
suggestion_type: str
target_type: str
target_id: str
target_version_at_submission: int
status: str
author_name: str
final_reviewer_id: Optional[str]
change_cmd: Dict[str, change_domain.AcceptableChangeDictTypes]
score_category: str
language_code: Optional[str]
last_updated: float
created_on: float
edited_by_reviewer: bool
class SuggestionEditStateContentUnitTests(test_utils.GenericTestBase):
"""Tests for the SuggestionEditStateContent class."""
AUTHOR_EMAIL: Final = 'author@example.com'
REVIEWER_EMAIL: Final = 'reviewer@example.com'
ASSIGNED_REVIEWER_EMAIL: Final = 'assigned_reviewer@example.com'
fake_date: datetime.datetime = datetime.datetime(2016, 4, 10, 0, 0, 0, 0)
def setUp(self) -> None:
super().setUp()
self.signup(self.AUTHOR_EMAIL, 'author')
self.author_id = self.get_user_id_from_email(self.AUTHOR_EMAIL)
self.signup(self.REVIEWER_EMAIL, 'reviewer')
self.reviewer_id = self.get_user_id_from_email(self.REVIEWER_EMAIL)
self.suggestion_dict: SuggestionEditStateContentDict = {
'suggestion_id': 'exploration.exp1.thread1',
'suggestion_type': (
feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT),
'target_type': feconf.ENTITY_TYPE_EXPLORATION,
'target_id': 'exp1',
'target_version_at_submission': 1,
'status': suggestion_models.STATUS_ACCEPTED,
'author_name': 'author',
'final_reviewer_id': self.reviewer_id,
'change_cmd': {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': 'state_1',
'new_value': 'new suggestion content',
'old_value': None
},
'score_category': 'content.Algebra',
'language_code': None,
'last_updated': utils.get_time_in_millisecs(self.fake_date),
'created_on': utils.get_time_in_millisecs(self.fake_date),
'edited_by_reviewer': False
}
def test_create_suggestion_edit_state_content(self) -> None:
expected_suggestion_dict = self.suggestion_dict
observed_suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date
)
self.assertDictEqual(
observed_suggestion.to_dict(), expected_suggestion_dict)
def test_validate_suggestion_edit_state_content(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
def test_get_score_part_helper_methods(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
self.assertEqual(suggestion.get_score_type(), 'content')
self.assertEqual(suggestion.get_score_sub_type(), 'Algebra')
def test_validate_suggestion_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.suggestion_type = 'invalid_suggestion_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected suggestion_type to be among allowed choices'
):
suggestion.validate()
def test_validate_target_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.target_type = 'invalid_target_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected target_type to be among allowed choices'
):
suggestion.validate()
def test_validate_target_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.target_id = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected target_id to be a string'
):
suggestion.validate()
def test_validate_target_version_at_submission(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.target_version_at_submission = 'invalid_version' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected target_version_at_submission to be an int'
):
suggestion.validate()
def test_validate_status(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.status = 'invalid_status'
with self.assertRaisesRegex(
utils.ValidationError, 'Expected status to be among allowed choices'
):
suggestion.validate()
def test_validate_author_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.author_id = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected author_id to be a string'
):
suggestion.validate()
def test_validate_author_id_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.author_id = self.PSEUDONYMOUS_ID
suggestion.validate()
suggestion.author_id = ''
with self.assertRaisesRegex(
utils.ValidationError,
'Expected author_id to be in a valid user ID format'
):
suggestion.validate()
def test_validate_final_reviewer_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.final_reviewer_id = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected final_reviewer_id to be a string'
):
suggestion.validate()
def test_validate_final_reviewer_id_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.final_reviewer_id = self.PSEUDONYMOUS_ID
suggestion.validate()
suggestion.final_reviewer_id = ''
with self.assertRaisesRegex(
utils.ValidationError,
'Expected final_reviewer_id to be in a valid user ID format'
):
suggestion.validate()
def test_validate_score_category(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.score_category = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected score_category to be a string'
):
suggestion.validate()
def test_validate_score_category_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'score.score_type.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected score_category to be of the form'
' score_type.score_sub_type'
):
suggestion.validate()
suggestion.score_category = 'invalid_score_category'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected score_category to be of the form'
' score_type.score_sub_type'
):
suggestion.validate()
def test_validate_score_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'invalid_score_type.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the first part of score_category to be among allowed'
' choices'
):
suggestion.validate()
def test_validate_change(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.change_cmd = {} # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected change_cmd to be an ExplorationChange'
):
suggestion.validate()
def test_validate_score_type_content(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'question.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the first part of score_category to be content'
):
suggestion.validate()
def test_validate_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.cmd = 'invalid_cmd'
with self.assertRaisesRegex(
utils.ValidationError, 'Expected cmd to be edit_state_property'
):
suggestion.validate()
def test_validate_change_property_name(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# Here we use MyPy ignore because 'property_name' can only accept
# 'content' string literal but here we are providing 'invalid_property'
# which causes MyPy to throw an error. Thus to avoid the error, we used
# ignore here.
suggestion.change_cmd.property_name = 'invalid_property' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected property_name to be content'
):
suggestion.validate()
def test_validate_language_code_fails_when_language_codes_do_not_match(
self
) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.language_code = 'wrong_language_code'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected language_code to be None, received wrong_language_code'
):
suggestion.validate()
def test_pre_accept_validate_state_name(self) -> None:
self.save_new_default_exploration('exp1', self.author_id)
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.change_cmd.state_name = 'Introduction'
suggestion.pre_accept_validate()
suggestion.change_cmd.state_name = 'invalid_state_name'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected invalid_state_name to be a valid state name'
):
suggestion.pre_accept_validate()
def test_populate_old_value_of_change_with_invalid_state(self) -> None:
self.save_new_default_exploration('exp1', self.author_id)
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.change_cmd.state_name = 'invalid_state_name'
self.assertIsNone(suggestion.change_cmd.old_value)
suggestion.populate_old_value_of_change()
self.assertIsNone(suggestion.change_cmd.old_value)
def test_pre_update_validate_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
change_cmd = {
'cmd': exp_domain.CMD_ADD_STATE,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': suggestion.change_cmd.state_name,
'new_value': 'new suggestion content',
'old_value': None
}
with self.assertRaisesRegex(
utils.ValidationError,
'The following extra attributes are present: new_value, '
'old_value, property_name'
):
suggestion.pre_update_validate(
exp_domain.EditExpStatePropertyContentCmd(change_cmd)
)
def test_pre_update_validate_change_property_name(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
change_cmd = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_PARAM_CHANGES,
'state_name': suggestion.change_cmd.state_name,
'new_value': 'new suggestion content',
'old_value': None
}
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd property_name must be equal to content'
):
suggestion.pre_update_validate(
exp_domain.EditExpStatePropertyContentCmd(change_cmd)
)
def test_pre_update_validate_change_state_name(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
change_cmd = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': 'invalid_state',
'new_value': 'new suggestion content',
'old_value': None
}
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd state_name must be equal to state_1'
):
suggestion.pre_update_validate(
exp_domain.EditExpStatePropertyContentCmd(change_cmd)
)
def test_pre_update_validate_change_new_value(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
new_content = state_domain.SubtitledHtml(
'content', '<p>new suggestion html</p>').to_dict()
suggestion.change_cmd.new_value = new_content
change_cmd: Dict[
str, Union[Optional[str], state_domain.SubtitledHtmlDict]
] = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': suggestion.change_cmd.state_name,
'new_value': new_content,
'old_value': None
}
with self.assertRaisesRegex(
utils.ValidationError, 'The new html must not match the old html'
):
suggestion.pre_update_validate(
exp_domain.EditExpStatePropertyContentCmd(change_cmd)
)
def test_pre_update_validate_non_equal_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionEditStateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd cmd must be equal to edit_state_property'
):
suggestion.pre_update_validate(
exp_domain.EditExpStatePropertyContentCmd({
'cmd': exp_domain.CMD_EDIT_EXPLORATION_PROPERTY,
'property_name': 'title',
'new_value': 'Exploration 1 Albert title'
})
)
def test_get_all_html_content_strings(self) -> None:
change_dict: Dict[str, Union[Optional[str], Dict[str, str]]] = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': 'state_1',
'new_value': {
'content_id': 'content',
'html': 'new suggestion content'
},
'old_value': None
}
suggestion = suggestion_registry.SuggestionEditStateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, change_dict,
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_all_html_content_strings()
expected_outcome_list = [u'new suggestion content']
self.assertEqual(expected_outcome_list, actual_outcome_list)
def test_convert_html_in_suggestion_change(self) -> None:
html_content = (
'<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
'mp;quot;+,-,-,+&quot;"></oppia-noninteractive-math>')
expected_html_content = (
'<p>Value</p><oppia-noninteractive-math math_content-with-value='
'"{&quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &'
'amp;quot;svg_filename&quot;: &quot;&quot;}"></oppia'
'-noninteractive-math>')
change_cmd: Dict[str, Union[str, Dict[str, str]]] = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': 'Introduction',
'new_value': {
'content_id': 'content',
'html': '<p>suggestion</p>'
},
'old_value': {
'content_id': 'content',
'html': html_content
}
}
suggestion = suggestion_registry.SuggestionEditStateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, change_cmd,
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.convert_html_in_suggestion_change(
html_validation_service.
add_math_content_to_math_rte_components)
# Ruling out the possibility of any other type for mypy type checking.
assert isinstance(suggestion.change_cmd.old_value, dict)
self.assertEqual(
suggestion.change_cmd.old_value['html'], expected_html_content)
def test_get_target_entity_html_strings_returns_expected_strings(
self
) -> None:
change_dict: Dict[str, Union[str, Dict[str, str]]] = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': 'state_1',
'new_value': {
'content_id': 'content',
'html': 'new suggestion content'
},
'old_value': {
'content_id': 'content',
'html': 'Old content.'
}
}
suggestion = suggestion_registry.SuggestionEditStateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, change_dict,
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_target_entity_html_strings()
expected_outcome_list = [u'Old content.']
self.assertEqual(expected_outcome_list, actual_outcome_list)
def test_get_target_entity_html_with_none_old_value(self) -> None:
change_dict: Dict[str, Union[Optional[str], Dict[str, str]]] = {
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'state_name': 'state_1',
'new_value': {
'content_id': 'content',
'html': 'new suggestion content'
},
'old_value': None
}
suggestion = suggestion_registry.SuggestionEditStateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, change_dict,
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_target_entity_html_strings()
self.assertEqual(actual_outcome_list, [])
class SuggestionTranslateContentUnitTests(test_utils.GenericTestBase):
"""Tests for the SuggestionEditStateContent class."""
AUTHOR_EMAIL: Final = 'author@example.com'
REVIEWER_EMAIL: Final = 'reviewer@example.com'
ASSIGNED_REVIEWER_EMAIL: Final = 'assigned_reviewer@example.com'
fake_date: datetime.datetime = datetime.datetime(2016, 4, 10, 0, 0, 0, 0)
def setUp(self) -> None:
super().setUp()
self.signup(self.AUTHOR_EMAIL, 'author')
self.author_id = self.get_user_id_from_email(self.AUTHOR_EMAIL)
self.signup(self.REVIEWER_EMAIL, 'reviewer')
self.reviewer_id = self.get_user_id_from_email(self.REVIEWER_EMAIL)
self.suggestion_dict: suggestion_registry.BaseSuggestionDict = {
'suggestion_id': 'exploration.exp1.thread1',
'suggestion_type': (
feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
'target_type': feconf.ENTITY_TYPE_EXPLORATION,
'target_id': 'exp1',
'target_version_at_submission': 1,
'status': suggestion_models.STATUS_ACCEPTED,
'author_name': 'author',
'final_reviewer_id': self.reviewer_id,
'change_cmd': {
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'Introduction',
'content_id': 'content',
'language_code': 'hi',
'content_html': '<p>This is a content.</p>',
'translation_html': '<p>This is translated html.</p>',
'data_format': 'html'
},
'score_category': 'translation.Algebra',
'language_code': 'hi',
'last_updated': utils.get_time_in_millisecs(self.fake_date),
'created_on': utils.get_time_in_millisecs(self.fake_date),
'edited_by_reviewer': False
}
opportunity_models.ExplorationOpportunitySummaryModel(
id='exp1',
topic_id='Topic1',
topic_name='New Topic',
story_id='Story1',
story_title='New Story',
chapter_title='New chapter',
content_count=10,
translation_counts={},
incomplete_translation_language_codes=[
language['id']
for language in constants.SUPPORTED_AUDIO_LANGUAGES
],
language_codes_needing_voice_artists=['en']
).put()
def test_pre_update_validate_fails_for_invalid_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False,
self.fake_date, self.fake_date)
change = {
'cmd': exp_domain.CMD_DELETE_STATE,
'state_name': 'Introduction'
}
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd cmd must be equal to %s' % (
exp_domain.CMD_ADD_WRITTEN_TRANSLATION)
):
suggestion.pre_update_validate(exp_domain.ExplorationChange(change))
def test_pre_update_validate_change_state_name(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False,
self.fake_date, self.fake_date)
change = {
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'State 1',
'content_id': 'content',
'language_code': 'hi',
'content_html': '<p>This is a content.</p>',
'translation_html': '<p>This is the updated translated html.</p>',
'data_format': 'html'
}
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd state_name must be equal to Introduction'
):
suggestion.pre_update_validate(exp_domain.ExplorationChange(change))
def test_pre_update_validate_change_language_code(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False,
self.fake_date, self.fake_date)
change = {
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'Introduction',
'content_id': 'content',
'language_code': 'en',
'content_html': '<p>This is a content.</p>',
'translation_html': '<p>This is the updated translated html.</p>',
'data_format': 'html'
}
with self.assertRaisesRegex(
utils.ValidationError,
'The language code must be equal to hi'
):
suggestion.pre_update_validate(exp_domain.ExplorationChange(change))
def test_pre_update_validate_change_content_html(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False,
self.fake_date, self.fake_date)
change = {
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'Introduction',
'content_id': 'content',
'language_code': 'en',
'content_html': '<p>This is the changed content.</p>',
'translation_html': '<p>This is the updated translated html.</p>',
'data_format': 'html'
}
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd content_html must be equal to <p>This is a ' +
'content.</p>'
):
suggestion.pre_update_validate(
exp_domain.ExplorationChange(change))
def test_create_suggestion_add_translation(self) -> None:
expected_suggestion_dict = self.suggestion_dict
observed_suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date
)
self.assertDictEqual(
observed_suggestion.to_dict(), expected_suggestion_dict)
def test_validate_suggestion_add_translation(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
def test_get_score_part_helper_methods(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
self.assertEqual(suggestion.get_score_type(), 'translation')
self.assertEqual(suggestion.get_score_sub_type(), 'Algebra')
def test_validate_suggestion_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.suggestion_type = 'invalid_suggestion_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected suggestion_type to be among allowed choices'
):
suggestion.validate()
def test_validate_target_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.target_type = 'invalid_target_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected target_type to be among allowed choices'
):
suggestion.validate()
def test_validate_target_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.target_id = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected target_id to be a string'
):
suggestion.validate()
def test_validate_target_version_at_submission(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.target_version_at_submission = 'invalid_version' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected target_version_at_submission to be an int'
):
suggestion.validate()
def test_validate_status(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.status = 'invalid_status'
with self.assertRaisesRegex(
utils.ValidationError, 'Expected status to be among allowed choices'
):
suggestion.validate()
def test_validate_author_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.author_id = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected author_id to be a string'
):
suggestion.validate()
def test_validate_author_id_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.author_id = ''
with self.assertRaisesRegex(
utils.ValidationError,
'Expected author_id to be in a valid user ID format.'
):
suggestion.validate()
def test_validate_final_reviewer_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.final_reviewer_id = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected final_reviewer_id to be a string'
):
suggestion.validate()
def test_validate_final_reviewer_id_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.final_reviewer_id = ''
with self.assertRaisesRegex(
utils.ValidationError,
'Expected final_reviewer_id to be in a valid user ID format'
):
suggestion.validate()
def test_validate_score_category(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.score_category = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected score_category to be a string'
):
suggestion.validate()
def test_validate_score_category_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'score.score_type.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected score_category to be of the form'
' score_type.score_sub_type'
):
suggestion.validate()
suggestion.score_category = 'invalid_score_category'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected score_category to be of the form'
' score_type.score_sub_type'
):
suggestion.validate()
def test_validate_score_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'invalid_score_type.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the first part of score_category to be among allowed'
' choices'
):
suggestion.validate()
def test_validate_change(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.change_cmd = {} # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected change_cmd to be an ExplorationChange'
):
suggestion.validate()
def test_validate_score_type_translation(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'question.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the first part of score_category to be translation'
):
suggestion.validate()
def test_validate_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.cmd = 'invalid_cmd'
with self.assertRaisesRegex(
utils.ValidationError, 'Expected cmd to be add_written_translation'
):
suggestion.validate()
def test_validate_translation_html_rte_tags(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.translation_html = (
'<oppia-noninteractive-image></oppia-noninteractive-image>')
with self.assertRaisesRegex(
utils.ValidationError,
'Image tag does not have \'alt-with-value\' attribute.'
):
suggestion.validate()
def test_validate_language_code_fails_when_language_codes_do_not_match(
self
) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
expected_language_code = (
expected_suggestion_dict['change_cmd']['language_code']
)
suggestion.validate()
suggestion.language_code = 'wrong_language_code'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected language_code to be %s, '
'received wrong_language_code' % expected_language_code
):
suggestion.validate()
def test_validate_language_code_fails_when_language_code_is_set_to_none(
self
) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.language_code = None # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'language_code cannot be None'
):
suggestion.validate()
def test_validate_change_with_invalid_language_code_fails_validation(
self
) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.language_code = 'invalid_code'
with self.assertRaisesRegex(
utils.ValidationError, 'Invalid language_code: invalid_code'
):
suggestion.validate()
def test_pre_accept_validate_state_name(self) -> None:
self.save_new_default_exploration('exp1', self.author_id)
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
exp_services.update_exploration(
self.author_id, 'exp1', [
exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_CONTENT,
'new_value': {
'content_id': 'content',
'html': '<p>This is a content.</p>'
},
'state_name': 'Introduction',
})
], 'Added state')
suggestion.change_cmd.state_name = 'Introduction'
suggestion.pre_accept_validate()
suggestion.change_cmd.state_name = 'invalid_state_name'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected invalid_state_name to be a valid state name'
):
suggestion.pre_accept_validate()
def test_accept_suggestion_adds_translation_in_exploration(self) -> None:
exp = self.save_new_default_exploration('exp1', self.author_id)
translations = (
translation_fetchers.get_all_entity_translations_for_entity(
feconf.TranslatableEntityType.EXPLORATION,
exp.id, exp.version))
self.assertEqual(len(translations), 0)
suggestion = suggestion_registry.SuggestionTranslateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, self.suggestion_dict['change_cmd'],
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.accept(
'Accepted suggestion by translator: Add translation change.')
translations = (
translation_fetchers.get_all_entity_translations_for_entity(
feconf.TranslatableEntityType.EXPLORATION,
exp.id, exp.version))
self.assertEqual(len(translations), 1)
self.assertEqual(translations[0].language_code, 'hi')
self.assertEqual(len(translations[0].translations), 1)
def test_accept_suggestion_with_set_of_string_adds_translation(
self
) -> None:
exp = self.save_new_default_exploration('exp1', self.author_id)
translations = (
translation_fetchers.get_all_entity_translations_for_entity(
feconf.TranslatableEntityType.EXPLORATION,
exp.id, exp.version))
self.assertEqual(len(translations), 0)
suggestion = suggestion_registry.SuggestionTranslateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id,
{
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'Introduction',
'content_id': 'content',
'language_code': 'hi',
'content_html': ['text1', 'text2'],
'translation_html': ['translated text1', 'translated text2'],
'data_format': 'set_of_normalized_string'
},
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.accept(
'Accepted suggestion by translator: Add translation change.')
translations = (
translation_fetchers.get_all_entity_translations_for_entity(
feconf.TranslatableEntityType.EXPLORATION, exp.id, exp.version
)
)
self.assertEqual(len(translations), 1)
self.assertEqual(translations[0].language_code, 'hi')
self.assertEqual(len(translations[0].translations), 1)
def test_accept_suggestion_with_psedonymous_author_adds_translation(
self
) -> None:
exp = self.save_new_default_exploration('exp1', self.author_id)
translations = (
translation_fetchers.get_all_entity_translations_for_entity(
feconf.TranslatableEntityType.EXPLORATION, exp.id, exp.version)
)
self.assertEqual(len(translations), 0)
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionTranslateContent(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.PSEUDONYMOUS_ID,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.accept(
'Accepted suggestion by translator: Add translation change.')
translations = (
translation_fetchers.get_all_entity_translations_for_entity(
feconf.TranslatableEntityType.EXPLORATION, exp.id, exp.version
)
)
self.assertEqual(len(translations), 1)
self.assertEqual(translations[0].language_code, 'hi')
self.assertEqual(len(translations[0].translations), 1)
def test_get_all_html_content_strings(self) -> None:
suggestion = suggestion_registry.SuggestionTranslateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, self.suggestion_dict['change_cmd'],
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_all_html_content_strings()
expected_outcome_list = [
u'<p>This is translated html.</p>', u'<p>This is a content.</p>']
self.assertEqual(expected_outcome_list, actual_outcome_list)
def test_get_all_html_content_strings_for_content_lists(self) -> None:
suggestion = suggestion_registry.SuggestionTranslateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id,
{
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'Introduction',
'content_id': 'content',
'language_code': 'hi',
'content_html': ['text1', 'text2'],
'translation_html': ['translated text1', 'translated text2'],
'data_format': 'set_of_normalized_string'
},
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_all_html_content_strings()
expected_outcome_list = [
'translated text1', 'translated text2', 'text1', 'text2']
self.assertEqual(expected_outcome_list, actual_outcome_list)
def test_get_target_entity_html_strings_returns_expected_strings(
self
) -> None:
suggestion = suggestion_registry.SuggestionTranslateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, self.suggestion_dict['change_cmd'],
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_target_entity_html_strings()
expected_outcome_list = (
[self.suggestion_dict['change_cmd']['content_html']])
self.assertEqual(expected_outcome_list, actual_outcome_list)
def test_convert_html_in_suggestion_change(self) -> None:
html_content = (
'<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
'mp;quot;+,-,-,+&quot;"></oppia-noninteractive-math>')
expected_html_content = (
'<p>Value</p><oppia-noninteractive-math math_content-with-value='
'"{&quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &'
'amp;quot;svg_filename&quot;: &quot;&quot;}"></oppia'
'-noninteractive-math>')
change_dict = {
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'Introduction',
'content_id': 'content',
'language_code': 'hi',
'content_html': html_content,
'translation_html': '<p>This is translated html.</p>',
'data_format': 'html'
}
suggestion = suggestion_registry.SuggestionTranslateContent(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, change_dict,
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.convert_html_in_suggestion_change(
html_validation_service.add_math_content_to_math_rte_components)
self.assertEqual(
suggestion.change_cmd.content_html, expected_html_content)
TestChangeDictType = Dict[
str,
Union[
str,
float,
Dict[str, Union[state_domain.StateDict, int, str, List[str]]]
]
]
class SuggestionAddQuestionTest(test_utils.GenericTestBase):
"""Tests for the SuggestionAddQuestion class."""
AUTHOR_EMAIL: Final = 'author@example.com'
REVIEWER_EMAIL: Final = 'reviewer@example.com'
ASSIGNED_REVIEWER_EMAIL: Final = 'assigned_reviewer@example.com'
fake_date: datetime.datetime = datetime.datetime(2016, 4, 10, 0, 0, 0, 0)
def setUp(self) -> None:
super().setUp()
content_id_generator = translation_domain.ContentIdGenerator()
self.signup(self.AUTHOR_EMAIL, 'author')
self.author_id = self.get_user_id_from_email(self.AUTHOR_EMAIL)
self.signup(self.REVIEWER_EMAIL, 'reviewer')
self.reviewer_id = self.get_user_id_from_email(self.REVIEWER_EMAIL)
self.suggestion_dict: suggestion_registry.BaseSuggestionDict = {
'suggestion_id': 'skill1.thread1',
'suggestion_type': feconf.SUGGESTION_TYPE_ADD_QUESTION,
'target_type': feconf.ENTITY_TYPE_SKILL,
'target_id': 'skill1',
'target_version_at_submission': 1,
'status': suggestion_models.STATUS_ACCEPTED,
'author_name': 'author',
'final_reviewer_id': self.reviewer_id,
'change_cmd': {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'linked_skill_ids': ['skill_1'],
'inapplicable_skill_misconception_ids': ['skillid12345-1'],
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3,
},
'score_category': 'question.topic_1',
'language_code': 'en',
'last_updated': utils.get_time_in_millisecs(self.fake_date),
'created_on': utils.get_time_in_millisecs(self.fake_date),
'edited_by_reviewer': False
}
def test_create_suggestion_add_question(self) -> None:
expected_suggestion_dict = self.suggestion_dict
observed_suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date
)
self.assertDictEqual(
observed_suggestion.to_dict(), expected_suggestion_dict)
def test_validate_suggestion_edit_state_content(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
def test_get_score_part_helper_methods(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
self.assertEqual(suggestion.get_score_type(), 'question')
self.assertEqual(suggestion.get_score_sub_type(), 'topic_1')
def test_validate_score_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.score_category = 'content.score_sub_type'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the first part of score_category to be "question"'
):
suggestion.validate()
def test_validate_change_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.change_cmd = 'invalid_change' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected change_cmd to be an '
'instance of QuestionSuggestionChange'
):
suggestion.validate()
def test_validate_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.cmd = None
with self.assertRaisesRegex(
utils.ValidationError, 'Expected change_cmd to contain cmd'
):
suggestion.validate()
def test_validate_change_cmd_type(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.cmd = 'invalid_cmd'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected cmd to be create_new_fully_specified_question'
):
suggestion.validate()
def test_validate_change_question_dict(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.change_cmd.question_dict = None # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected change_cmd to contain question_dict'
):
suggestion.validate()
def test_validate_change_question_state_data_schema_version(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# We are not setting value in suggestion.change.question_dict
# directly since pylint produces unsupported-assignment-operation
# error. The detailed analysis for the same can be checked
# in this issue: https://github.com/oppia/oppia/issues/7008.
assert isinstance(suggestion.change_cmd.question_dict, dict)
question_dict: question_domain.QuestionDict = (
suggestion.change_cmd.question_dict
)
question_dict['question_state_data_schema_version'] = 0
suggestion.change_cmd.question_dict = question_dict
with self.assertRaisesRegex(
utils.ValidationError,
'Expected question state schema version to be %s, '
'received 0' % feconf.CURRENT_STATE_SCHEMA_VERSION
):
suggestion.validate()
def test_validate_change_skill_difficulty_none(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.change_cmd.skill_difficulty = None # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected change_cmd to contain skill_difficulty'
):
suggestion.validate()
def test_validate_change_skill_difficulty_invalid_value(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.change_cmd.skill_difficulty = 0.4
with self.assertRaisesRegex(
utils.ValidationError,
'Expected change_cmd skill_difficulty to be one of '
):
suggestion.validate()
def test_pre_accept_validate_change_skill_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
skill_id = skill_services.get_new_skill_id()
self.save_new_skill(skill_id, self.author_id, description='description')
suggestion.change_cmd.skill_id = skill_id
suggestion.pre_accept_validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.change_cmd.skill_id = None # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected change_cmd to contain skill_id'
):
suggestion.pre_accept_validate()
def test_pre_accept_validate_change_invalid_skill_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
skill_id = skill_services.get_new_skill_id()
self.save_new_skill(skill_id, self.author_id, description='description')
suggestion.change_cmd.skill_id = skill_id
suggestion.pre_accept_validate()
suggestion.change_cmd.skill_id = skill_services.get_new_skill_id()
with self.assertRaisesRegex(
utils.ValidationError, 'The skill with the given id doesn\'t exist.'
):
suggestion.pre_accept_validate()
def test_populate_old_value_of_change(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
# Here we use MyPy ignore because method `populate_old_value_of_change`
# does not return any value but for testing purpose we are still
# comparing it's return value with None which causes MyPy to throw
# error. Thus to avoid the error, we used ignore here.
self.assertIsNone(suggestion.populate_old_value_of_change()) # type: ignore[func-returns-value]
def test_cannot_accept_suggestion_with_invalid_skill_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.change_cmd.skill_id = skill_services.get_new_skill_id()
with self.assertRaisesRegex(
utils.ValidationError,
'The skill with the given id doesn\'t exist.'
):
suggestion.accept('commit message')
def test_pre_update_validate_change_cmd(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
change = {
'cmd': question_domain.CMD_UPDATE_QUESTION_PROPERTY,
'property_name': question_domain.QUESTION_PROPERTY_LANGUAGE_CODE,
'new_value': 'bn',
'old_value': 'en'
}
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd cmd must be equal to '
'create_new_fully_specified_question'
):
suggestion.pre_update_validate(
question_domain.QuestionChange(change)) # type: ignore[arg-type]
def test_pre_update_validate_change_skill_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
content_id_generator = translation_domain.ContentIdGenerator()
change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_2'
}
with self.assertRaisesRegex(
utils.ValidationError,
'The new change_cmd skill_id must be equal to skill_1'
):
suggestion.pre_update_validate(
question_domain.CreateNewFullySpecifiedQuestionCmd(
change
)
)
def test_pre_update_validate_complains_if_nothing_changed(self) -> None:
content_id_generator = translation_domain.ContentIdGenerator()
change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3
}
suggestion = suggestion_registry.SuggestionAddQuestion(
'exploration.exp1.thread1', 'exp1', 1,
suggestion_models.STATUS_ACCEPTED, self.author_id,
self.reviewer_id, change,
'question.topic_1', 'en', False, self.fake_date, self.fake_date)
content_id_generator = translation_domain.ContentIdGenerator()
new_change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3
}
with self.assertRaisesRegex(
utils.ValidationError,
'At least one of the new skill_difficulty or question_dict '
'should be changed.'):
suggestion.pre_update_validate(
question_domain.CreateNewFullySpecifiedQuestionSuggestionCmd(
new_change
)
)
def test_pre_update_validate_accepts_a_change_in_skill_difficulty_only(
self
) -> None:
content_id_generator = translation_domain.ContentIdGenerator()
change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3
}
suggestion = suggestion_registry.SuggestionAddQuestion(
'exploration.exp1.thread1', 'exp1', 1,
suggestion_models.STATUS_ACCEPTED, self.author_id,
self.reviewer_id, change,
'question.topic_1', 'en', False, self.fake_date, self.fake_date)
content_id_generator = translation_domain.ContentIdGenerator()
new_change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.6
}
# Here we use MyPy ignore because method `pre_update_validate` does not
# return any value but for testing purpose we are still comparing it's
# return value with None which causes MyPy to throw error. Thus to avoid
# the error, we used ignore here.
self.assertEqual(
suggestion.pre_update_validate( # type: ignore[func-returns-value]
question_domain.CreateNewFullySpecifiedQuestionSuggestionCmd(
new_change
)
),
None
)
def test_pre_update_validate_accepts_a_change_in_state_data_only(
self
) -> None:
content_id_generator = translation_domain.ContentIdGenerator()
change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3
}
suggestion = suggestion_registry.SuggestionAddQuestion(
'exploration.exp1.thread1', 'exp1', 1,
suggestion_models.STATUS_ACCEPTED, self.author_id,
self.reviewer_id, change,
'question.topic_1', 'en', False, self.fake_date, self.fake_date)
content_id_generator = translation_domain.ContentIdGenerator()
new_change: ChangeType = {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': self._create_valid_question_data(
'default_state', content_id_generator).to_dict(),
'language_code': 'hi',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3
}
# Here we use MyPy ignore because method `pre_update_validate` does not
# return any value but for testing purpose we are still comparing it's
# return value with None which causes MyPy to throw error. Thus to avoid
# the error, we used ignore here.
self.assertEqual(
suggestion.pre_update_validate( # type: ignore[func-returns-value]
question_domain.CreateNewFullySpecifiedQuestionSuggestionCmd(
new_change
)
),
None
)
def test_validate_author_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.author_id = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected author_id to be a string'):
suggestion.validate()
def test_validate_author_id_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.author_id = ''
with self.assertRaisesRegex(
utils.ValidationError,
'Expected author_id to be in a valid user ID format.'):
suggestion.validate()
def test_validate_final_reviewer_id(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.final_reviewer_id = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected final_reviewer_id to be a string'):
suggestion.validate()
def test_validate_final_reviewer_id_format(self) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
suggestion.final_reviewer_id = ''
with self.assertRaisesRegex(
utils.ValidationError,
'Expected final_reviewer_id to be in a valid user ID format'):
suggestion.validate()
def test_validate_language_code_fails_when_language_codes_do_not_match(
self
) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
# Here we use cast because the value of `question_dict` key is a
# Union of all allowed change dict types. So, to narrow down the type
# to QuestionDict, we used assert here.
assert isinstance(
expected_suggestion_dict['change_cmd']['question_dict'], dict
)
expected_question_dict = (
cast(
question_domain.QuestionDict,
expected_suggestion_dict['change_cmd']['question_dict']
)
)
suggestion.validate()
expected_question_dict['language_code'] = 'wrong_language_code'
with self.assertRaisesRegex(
utils.ValidationError,
'Expected question language_code.wrong_language_code. to be same '
'as suggestion language_code.en.'
):
suggestion.validate()
def test_validate_language_code_fails_when_language_code_is_set_to_none(
self
) -> None:
expected_suggestion_dict = self.suggestion_dict
suggestion = suggestion_registry.SuggestionAddQuestion(
expected_suggestion_dict['suggestion_id'],
expected_suggestion_dict['target_id'],
expected_suggestion_dict['target_version_at_submission'],
expected_suggestion_dict['status'], self.author_id,
self.reviewer_id, expected_suggestion_dict['change_cmd'],
expected_suggestion_dict['score_category'],
expected_suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
suggestion.language_code = None # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected language_code to be en, received None'):
suggestion.validate()
def test_get_all_html_content_strings(self) -> None:
suggestion = suggestion_registry.SuggestionAddQuestion(
self.suggestion_dict['suggestion_id'],
self.suggestion_dict['target_id'],
self.suggestion_dict['target_version_at_submission'],
self.suggestion_dict['status'], self.author_id,
self.reviewer_id, self.suggestion_dict['change_cmd'],
self.suggestion_dict['score_category'],
self.suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
actual_outcome_list = suggestion.get_all_html_content_strings()
expected_outcome_list = [
u'', u'', u'<p>This is a hint.</p>', u'<p>This is a solution.</p>']
self.assertEqual(expected_outcome_list, actual_outcome_list)
def test_convert_html_in_suggestion_change(self) -> None:
html_content = (
'<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
'mp;quot;+,-,-,+&quot;"></oppia-noninteractive-math>')
expected_html_content = (
'<p>Value</p><oppia-noninteractive-math math_content-with-value='
'"{&quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &'
'amp;quot;svg_filename&quot;: &quot;&quot;}"></oppia'
'-noninteractive-math>')
answer_group = {
'outcome': {
'dest': None,
'dest_if_really_stuck': None,
'feedback': {
'content_id': 'feedback_1',
'html': ''
},
'labelled_as_correct': True,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None
},
'rule_specs': [{
'inputs': {
'x': 0
},
'rule_type': 'Equals'
}],
'training_data': [],
'tagged_skill_misconception_id': None
}
question_state_dict = {
'content': {
'content_id': 'content_1',
'html': html_content
},
'recorded_voiceovers': {
'voiceovers_mapping': {
'content_1': {},
'feedback_1': {},
'feedback_2': {},
'hint_1': {},
'solution': {}
}
},
'interaction': {
'answer_groups': [answer_group],
'confirmed_unclassified_answers': [],
'customization_args': {
'choices': {
'value': [{
'html': 'option 1',
'content_id': 'ca_choices_0'
}]
},
'showChoicesInShuffledOrder': {
'value': True
}
},
'default_outcome': {
'dest': None,
'dest_if_really_stuck': None,
'feedback': {
'content_id': 'feedback_2',
'html': 'Correct Answer'
},
'param_changes': [],
'refresher_exploration_id': None,
'labelled_as_correct': True,
'missing_prerequisite_skill_id': None
},
'hints': [{
'hint_content': {
'content_id': 'hint_1',
'html': 'Hint 1'
}
}],
'solution': {
'answer_is_exclusive': False,
'correct_answer': 0,
'explanation': {
'content_id': 'solution',
'html': '<p>This is a solution.</p>'
}
},
'id': 'MultipleChoiceInput'
},
'param_changes': [],
'solicit_answer_details': False,
'classifier_model_id': None
}
suggestion_dict: suggestion_registry.BaseSuggestionDict = {
'suggestion_id': 'skill1.thread1',
'suggestion_type': feconf.SUGGESTION_TYPE_ADD_QUESTION,
'target_type': feconf.ENTITY_TYPE_SKILL,
'target_id': 'skill1',
'target_version_at_submission': 1,
'status': suggestion_models.STATUS_ACCEPTED,
'author_name': 'author',
'final_reviewer_id': self.reviewer_id,
'change_cmd': {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': question_state_dict,
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'linked_skill_ids': ['skill_1'],
'inapplicable_skill_misconception_ids': ['skillid12345-1']
},
'skill_id': 'skill_1',
'skill_difficulty': 0.3,
},
'score_category': 'question.skill1',
'language_code': 'en',
'last_updated': utils.get_time_in_millisecs(self.fake_date),
'created_on': utils.get_time_in_millisecs(self.fake_date),
'edited_by_reviewer': False
}
suggestion = suggestion_registry.SuggestionAddQuestion(
suggestion_dict['suggestion_id'], suggestion_dict['target_id'],
suggestion_dict['target_version_at_submission'],
suggestion_dict['status'], self.author_id, self.reviewer_id,
suggestion_dict['change_cmd'], suggestion_dict['score_category'],
suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.convert_html_in_suggestion_change(
html_validation_service.add_math_content_to_math_rte_components)
# Ruling out the possibility of any other type for mypy type checking.
assert isinstance(suggestion.change_cmd.question_dict, dict)
question_dict: question_domain.QuestionDict = (
suggestion.change_cmd.question_dict
)
self.assertEqual(
question_dict['question_state_data']['content'][
'html'], expected_html_content)
def test_accept_suggestion_with_images(self) -> None:
html_content = (
'<p>Value</p><oppia-noninteractive-math math_content-with-value='
'"{&quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &'
'amp;quot;svg_filename&quot;: &quot;img.svg&quot;}">'
'</oppia-noninteractive-math>')
content_id_generator = translation_domain.ContentIdGenerator()
question_state_dict = self._create_valid_question_data(
'default_state', content_id_generator).to_dict()
question_state_dict['content']['html'] = html_content
with utils.open_file(
os.path.join(feconf.TESTS_DATA_DIR, 'test_svg.svg'),
'rb', encoding=None) as f:
raw_image = f.read()
image_context = feconf.IMAGE_CONTEXT_QUESTION_SUGGESTIONS
fs_services.save_original_and_compressed_versions_of_image(
'img.svg', image_context, 'skill1',
raw_image, 'image', False)
self.save_new_skill('skill1', self.author_id, description='description')
suggestion_dict: suggestion_registry.BaseSuggestionDict = {
'suggestion_id': 'skill1.thread1',
'suggestion_type': feconf.SUGGESTION_TYPE_ADD_QUESTION,
'target_type': feconf.ENTITY_TYPE_SKILL,
'target_id': 'skill1',
'target_version_at_submission': 1,
'status': suggestion_models.STATUS_ACCEPTED,
'author_name': 'author',
'final_reviewer_id': self.reviewer_id,
'change_cmd': {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': question_state_dict,
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'linked_skill_ids': ['skill_1'],
'inapplicable_skill_misconception_ids': [],
'next_content_id_index': (
content_id_generator.next_content_id_index)
},
'skill_id': 'skill1',
'skill_difficulty': 0.3,
},
'score_category': 'question.skill1',
'language_code': 'en',
'last_updated': utils.get_time_in_millisecs(self.fake_date),
'created_on': utils.get_time_in_millisecs(self.fake_date),
'edited_by_reviewer': False
}
suggestion = suggestion_registry.SuggestionAddQuestion(
suggestion_dict['suggestion_id'], suggestion_dict['target_id'],
suggestion_dict['target_version_at_submission'],
suggestion_dict['status'], self.author_id, self.reviewer_id,
suggestion_dict['change_cmd'], suggestion_dict['score_category'],
suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.accept('commit_message')
def test_accept_suggestion_with_image_region_interactions(self) -> None:
with utils.open_file(
os.path.join(feconf.TESTS_DATA_DIR, 'img.png'), 'rb',
encoding=None) as f:
original_image_content = f.read()
fs_services.save_original_and_compressed_versions_of_image(
'image.png', 'question_suggestions', 'skill1',
original_image_content, 'image', True)
image_and_region_ca_dict: domain.ImageAndRegionDict = {
'imagePath': 'image.png',
'labeledRegions': [
{
'label': 'Region1',
'region': {
'regionType': 'Rectangle',
'area': [
[
0.2644628099173554,
0.21807065217391305
],
[
0.9201101928374655,
0.8847373188405797
]
]
}
}
]
}
# Here, the expected type for `solution` key is SolutionDict but
# for testing purposes here we are providing None which causes
# MyPy to throw `Incompatible types` error. Thus to avoid the
# error, we used ignore here.
question_state_dict: state_domain.StateDict = {
'content': {
'html': '<p>Text</p>',
'content_id': 'content_0'
},
'classifier_model_id': None,
'linked_skill_id': None,
'interaction': {
'answer_groups': [
{
'rule_specs': [
{
'rule_type': 'IsInRegion',
'inputs': {'x': 'Region1'}
}
],
'outcome': {
'dest': None,
'dest_if_really_stuck': None,
'feedback': {
'html': '<p>assas</p>',
'content_id': 'feedback_2'
},
'labelled_as_correct': True,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None
},
'training_data': [],
'tagged_skill_misconception_id': None
}
],
'confirmed_unclassified_answers': [],
'customization_args': {
'imageAndRegions': {
'value': image_and_region_ca_dict
},
'highlightRegionsOnHover': {
'value': False
}
},
'default_outcome': {
'dest': None,
'dest_if_really_stuck': None,
'feedback': {
'html': '<p>wer</p>',
'content_id': 'default_outcome_1'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None
},
'hints': [
{
'hint_content': {
'html': '<p>assaas</p>',
'content_id': 'hint_3'
}
}
],
'id': 'ImageClickInput', 'solution': None
},
'param_changes': [],
'recorded_voiceovers': {
'voiceovers_mapping': {
'content_0': {},
'default_outcome_1': {},
'feedback_2': {},
'hint_3': {}
}
},
'solicit_answer_details': False,
'card_is_checkpoint': False,
}
suggestion_dict: suggestion_registry.BaseSuggestionDict = {
'suggestion_id': 'skill1.thread1',
'suggestion_type': feconf.SUGGESTION_TYPE_ADD_QUESTION,
'target_type': feconf.ENTITY_TYPE_SKILL,
'target_id': 'skill1',
'target_version_at_submission': 1,
'status': suggestion_models.STATUS_ACCEPTED,
'author_name': 'author',
'final_reviewer_id': self.reviewer_id,
'change_cmd': {
'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION,
'question_dict': {
'question_state_data': question_state_dict,
'language_code': 'en',
'question_state_data_schema_version': (
feconf.CURRENT_STATE_SCHEMA_VERSION),
'linked_skill_ids': ['skill1'],
'next_content_id_index': 4,
'inapplicable_skill_misconception_ids': []
},
'skill_id': 'skill1',
'skill_difficulty': 0.3,
},
'score_category': 'question.skill1',
'language_code': 'en',
'last_updated': utils.get_time_in_millisecs(self.fake_date),
'created_on': utils.get_time_in_millisecs(self.fake_date),
'edited_by_reviewer': False
}
self.save_new_skill(
'skill1', self.author_id, description='description')
suggestion = suggestion_registry.SuggestionAddQuestion(
suggestion_dict['suggestion_id'], suggestion_dict['target_id'],
suggestion_dict['target_version_at_submission'],
suggestion_dict['status'], self.author_id, self.reviewer_id,
suggestion_dict['change_cmd'], suggestion_dict['score_category'],
suggestion_dict['language_code'], False, self.fake_date,
self.fake_date)
suggestion.accept('commit_message')
question = question_services.get_questions_by_skill_ids(
1, ['skill1'], False)[0]
destination_fs = fs_services.GcsFileSystem(
feconf.ENTITY_TYPE_QUESTION, question.id)
self.assertTrue(destination_fs.isfile('image/%s' % 'image.png'))
self.assertEqual(
suggestion.status,
suggestion_models.STATUS_ACCEPTED)
def test_contructor_updates_state_shema_in_change_cmd(self) -> None:
score_category = (
suggestion_models.SCORE_TYPE_QUESTION +
suggestion_models.SCORE_CATEGORY_DELIMITER + 'skill_id')
change: TestChangeDictType = {
'cmd': (
question_domain
.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION),
'question_dict': {
'question_state_data': self.VERSION_27_STATE_DICT,
'question_state_data_schema_version': 27,
'language_code': 'en',
'linked_skill_ids': ['skill_id'],
'inapplicable_skill_misconception_ids': []
},
'skill_id': 'skill_id',
'skill_difficulty': 0.3
}
# Ruling out the possibility of any other type for mypy type checking.
assert isinstance(change['question_dict'], dict)
self.assertEqual(
change['question_dict']['question_state_data_schema_version'], 27)
suggestion = suggestion_registry.SuggestionAddQuestion(
'suggestionId', 'target_id', 1, suggestion_models.STATUS_IN_REVIEW,
self.author_id, 'test_reviewer', change, score_category, 'en',
False, self.fake_date, self.fake_date)
# Ruling out the possibility of any other type for mypy type checking.
assert isinstance(suggestion.change_cmd.question_dict, dict)
self.assertEqual(
suggestion.change_cmd.question_dict[
'question_state_data_schema_version'],
feconf.CURRENT_STATE_SCHEMA_VERSION)
def test_contructor_raise_exception_for_invalid_state_shema_version(
self
) -> None:
score_category = (
suggestion_models.SCORE_TYPE_QUESTION +
suggestion_models.SCORE_CATEGORY_DELIMITER + 'skill_id')
change: TestChangeDictType = {
'cmd': (
question_domain
.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION),
'question_dict': {
'question_state_data': self.VERSION_27_STATE_DICT,
'question_state_data_schema_version': 23,
'language_code': 'en',
'linked_skill_ids': ['skill_id'],
'inapplicable_skill_misconception_ids': []
},
'skill_id': 'skill_id',
'skill_difficulty': 0.3
}
# Ruling out the possibility of any other type for mypy type checking.
assert isinstance(change['question_dict'], dict)
self.assertEqual(
change['question_dict']['question_state_data_schema_version'], 23)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected state schema version to be in between 25'
):
suggestion_registry.SuggestionAddQuestion(
'suggestionId', 'target_id', 1,
suggestion_models.STATUS_IN_REVIEW, self.author_id,
'test_reviewer', change, score_category, 'en', False,
self.fake_date, self.fake_date)
class CommunityContributionStatsUnitTests(test_utils.GenericTestBase):
"""Tests for the CommunityContributionStats class."""
translation_reviewer_counts_by_lang_code: Dict[str, int] = {
'hi': 0,
'en': 1
}
translation_suggestion_counts_by_lang_code: Dict[str, int] = {
'fr': 6,
'en': 5
}
question_reviewer_count: int = 1
question_suggestion_count: int = 4
negative_count: int = -1
non_integer_count: str = 'non_integer_count'
sample_language_code: str = 'en'
invalid_language_code: str = 'invalid'
def _assert_community_contribution_stats_is_in_default_state(self) -> None:
"""Checks if the community contribution stats is in its default
state.
"""
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self.assertEqual(
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
), {})
self.assertEqual(
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
), {})
self.assertEqual(
community_contribution_stats.question_reviewer_count, 0)
self.assertEqual(
community_contribution_stats.question_suggestion_count, 0)
def test_initial_object_with_valid_arguments_has_correct_properties(
self
) -> None:
community_contribution_stats = (
suggestion_registry.CommunityContributionStats(
self.translation_reviewer_counts_by_lang_code,
self.translation_suggestion_counts_by_lang_code,
self.question_reviewer_count,
self.question_suggestion_count
)
)
community_contribution_stats.validate()
self.assertEqual(
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
),
self.translation_reviewer_counts_by_lang_code)
self.assertEqual(
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
),
self.translation_suggestion_counts_by_lang_code
)
self.assertEqual(
community_contribution_stats.question_reviewer_count,
self.question_reviewer_count
)
self.assertEqual(
community_contribution_stats.question_suggestion_count,
self.question_suggestion_count
)
def test_set_translation_reviewer_count_for_lang_code_updates_empty_dict(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self._assert_community_contribution_stats_is_in_default_state()
(
community_contribution_stats
.set_translation_reviewer_count_for_language_code(
self.sample_language_code, 2)
)
self.assertDictEqual(
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
),
{self.sample_language_code: 2}
)
def test_set_translation_reviewer_count_for_lang_code_updates_count_value(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self._assert_community_contribution_stats_is_in_default_state()
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
) = {self.sample_language_code: 1}
(
community_contribution_stats
.set_translation_reviewer_count_for_language_code(
self.sample_language_code, 2)
)
self.assertDictEqual(
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
),
{self.sample_language_code: 2}
)
def test_set_translation_reviewer_count_for_lang_code_adds_new_lang_key(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self._assert_community_contribution_stats_is_in_default_state()
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
) = {'en': 1}
(
community_contribution_stats
.set_translation_reviewer_count_for_language_code('hi', 2)
)
self.assertDictEqual(
(
community_contribution_stats
.translation_reviewer_counts_by_lang_code
),
{'en': 1, 'hi': 2}
)
def test_set_translation_suggestion_count_for_lang_code_updates_empty_dict(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self._assert_community_contribution_stats_is_in_default_state()
(
community_contribution_stats
.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 2)
)
self.assertDictEqual(
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
), {self.sample_language_code: 2}
)
def test_set_translation_suggestion_count_for_lang_code_updates_count_value(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self._assert_community_contribution_stats_is_in_default_state()
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
) = {self.sample_language_code: 1}
(
community_contribution_stats
.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 2)
)
self.assertDictEqual(
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
),
{self.sample_language_code: 2}
)
def test_set_translation_suggestion_count_for_lang_code_adds_new_lang_key(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
self._assert_community_contribution_stats_is_in_default_state()
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
) = {'en': 1}
(
community_contribution_stats
.set_translation_suggestion_count_for_language_code('hi', 2)
)
self.assertDictEqual(
(
community_contribution_stats
.translation_suggestion_counts_by_lang_code
),
{'en': 1, 'hi': 2}
)
def test_get_translation_language_codes_that_need_reviewers_for_one_lang(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 1)
language_codes_that_need_reviewers = (
stats.get_translation_language_codes_that_need_reviewers()
)
self.assertEqual(
language_codes_that_need_reviewers, {self.sample_language_code})
def test_get_translation_language_codes_that_need_reviewers_for_multi_lang(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_suggestion_count_for_language_code('hi', 1)
stats.set_translation_suggestion_count_for_language_code('fr', 1)
language_codes_that_need_reviewers = (
stats.get_translation_language_codes_that_need_reviewers()
)
self.assertEqual(
language_codes_that_need_reviewers, {'hi', 'fr'})
def test_get_translation_language_codes_that_need_reviewers_for_no_lang(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
language_codes_that_need_reviewers = (
stats.get_translation_language_codes_that_need_reviewers()
)
self.assertEqual(
language_codes_that_need_reviewers, set())
def test_translation_reviewers_are_needed_if_suggestions_but_no_reviewers(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 1)
self.assertTrue(
stats.are_translation_reviewers_needed_for_lang_code(
self.sample_language_code))
def test_translation_reviewers_are_needed_if_num_suggestions_past_max(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 2)
stats.set_translation_reviewer_count_for_language_code(
self.sample_language_code, 1)
swap_platform_parameter_value = self.swap_to_always_return(
platform_parameter_services,
'get_platform_parameter_value',
1
)
with swap_platform_parameter_value:
reviewers_are_needed = (
stats.are_translation_reviewers_needed_for_lang_code(
self.sample_language_code))
self.assertTrue(reviewers_are_needed)
def test_translation_reviewers_not_needed_if_num_suggestions_eqs_max(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 2)
stats.set_translation_reviewer_count_for_language_code(
self.sample_language_code, 2)
swap_platform_parameter_value = self.swap_to_always_return(
platform_parameter_services,
'get_platform_parameter_value',
1
)
with swap_platform_parameter_value:
reviewers_are_needed = (
stats.are_translation_reviewers_needed_for_lang_code(
self.sample_language_code))
self.assertFalse(reviewers_are_needed)
def test_translation_reviewers_not_needed_if_num_suggestions_less_max(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_suggestion_count_for_language_code(
self.sample_language_code, 1)
stats.set_translation_reviewer_count_for_language_code(
self.sample_language_code, 2)
swap_platform_parameter_value = self.swap_to_always_return(
platform_parameter_services,
'get_platform_parameter_value',
1
)
with swap_platform_parameter_value:
reviewers_are_needed = (
stats.are_translation_reviewers_needed_for_lang_code(
self.sample_language_code))
self.assertFalse(reviewers_are_needed)
def test_translation_reviewers_not_needed_if_reviewers_and_no_sugestions(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.set_translation_reviewer_count_for_language_code(
self.sample_language_code, 1)
self.assertFalse(
stats.are_translation_reviewers_needed_for_lang_code(
self.sample_language_code))
def test_translation_reviewers_not_needed_if_no_reviewers_no_sugestions(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
self._assert_community_contribution_stats_is_in_default_state()
self.assertFalse(
stats.are_translation_reviewers_needed_for_lang_code(
self.sample_language_code))
def test_question_reviewers_are_needed_if_suggestions_zero_reviewers(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.question_suggestion_count = 1
self.assertTrue(stats.are_question_reviewers_needed())
def test_question_reviewers_are_needed_if_num_suggestions_past_max(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.question_suggestion_count = 2
stats.question_reviewer_count = 1
swap_platform_parameter_value = self.swap_to_always_return(
platform_parameter_services,
'get_platform_parameter_value',
1
)
with swap_platform_parameter_value:
reviewers_are_needed = stats.are_question_reviewers_needed()
self.assertTrue(reviewers_are_needed)
def test_question_reviewers_not_needed_if_num_suggestions_eqs_max(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.question_suggestion_count = 2
stats.question_reviewer_count = 2
swap_platform_parameter_value = self.swap_to_always_return(
platform_parameter_services,
'get_platform_parameter_value',
1
)
with swap_platform_parameter_value:
reviewers_are_needed = stats.are_question_reviewers_needed()
self.assertFalse(reviewers_are_needed)
def test_question_reviewers_not_needed_if_num_suggestions_less_max(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
stats.question_suggestion_count = 1
stats.question_reviewer_count = 2
swap_platform_parameter_value = self.swap_to_always_return(
platform_parameter_services,
'get_platform_parameter_value',
1
)
with swap_platform_parameter_value:
reviewers_are_needed = stats.are_question_reviewers_needed()
self.assertFalse(reviewers_are_needed)
def test_question_reviewers_not_needed_if_no_reviewers_no_sugestions(
self
) -> None:
stats = suggestion_services.get_community_contribution_stats()
self._assert_community_contribution_stats_is_in_default_state()
self.assertFalse(stats.are_question_reviewers_needed())
def test_validate_translation_reviewer_counts_fails_for_negative_counts(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
(
community_contribution_stats
.set_translation_reviewer_count_for_language_code(
self.sample_language_code, self.negative_count)
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the translation reviewer count to be non-negative for '
'%s language code, received: %s.' % (
self.sample_language_code, self.negative_count)
):
community_contribution_stats.validate()
def test_validate_translation_suggestion_counts_fails_for_negative_counts(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
(
community_contribution_stats
.set_translation_suggestion_count_for_language_code(
self.sample_language_code, self.negative_count)
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the translation suggestion count to be non-negative for '
'%s language code, received: %s.' % (
self.sample_language_code, self.negative_count)
):
community_contribution_stats.validate()
def test_validate_question_reviewer_count_fails_for_negative_count(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
community_contribution_stats.question_reviewer_count = (
self.negative_count
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the question reviewer count to be non-negative, '
'received: %s.' % (
community_contribution_stats.question_reviewer_count)
):
community_contribution_stats.validate()
def test_validate_question_suggestion_count_fails_for_negative_count(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
community_contribution_stats.question_suggestion_count = (
self.negative_count
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the question suggestion count to be non-negative, '
'received: %s.' % (
community_contribution_stats.question_suggestion_count)
):
community_contribution_stats.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
def test_validate_translation_reviewer_counts_fails_for_non_integer_counts(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
(
community_contribution_stats
.set_translation_reviewer_count_for_language_code(
self.sample_language_code, self.non_integer_count) # type: ignore[arg-type]
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the translation reviewer count to be an integer for '
'%s language code, received: %s.' % (
self.sample_language_code, self.non_integer_count)
):
community_contribution_stats.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
def test_validate_translation_suggestion_counts_fails_for_non_integer_count(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
(
community_contribution_stats
.set_translation_suggestion_count_for_language_code(
self.sample_language_code, self.non_integer_count) # type: ignore[arg-type]
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the translation suggestion count to be an integer for '
'%s language code, received: %s.' % (
self.sample_language_code, self.non_integer_count)
):
community_contribution_stats.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
def test_validate_question_reviewer_count_fails_for_non_integer_count(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
community_contribution_stats.question_reviewer_count = (
self.non_integer_count # type: ignore[assignment]
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the question reviewer count to be an integer, '
'received: %s.' % (
community_contribution_stats.question_reviewer_count)
):
community_contribution_stats.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type the
# codebase we plan to get rid of the tests that intentionally test wrong
# inputs that we can normally catch by typing.
def test_validate_question_suggestion_count_fails_for_non_integer_count(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
community_contribution_stats.question_suggestion_count = (
self.non_integer_count # type: ignore[assignment]
)
with self.assertRaisesRegex(
utils.ValidationError,
'Expected the question suggestion count to be an integer, '
'received: %s.' % (
community_contribution_stats.question_suggestion_count)
):
community_contribution_stats.validate()
def test_validate_translation_reviewer_counts_fails_for_invalid_lang_code(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
(
community_contribution_stats
.set_translation_reviewer_count_for_language_code(
self.invalid_language_code, 1)
)
with self.assertRaisesRegex(
utils.ValidationError,
'Invalid language code for the translation reviewer counts: '
'%s.' % self.invalid_language_code
):
community_contribution_stats.validate()
def test_validate_translation_suggestion_counts_fails_for_invalid_lang_code(
self
) -> None:
community_contribution_stats = (
suggestion_services.get_community_contribution_stats()
)
(
community_contribution_stats
.set_translation_suggestion_count_for_language_code(
self.invalid_language_code, 1)
)
with self.assertRaisesRegex(
utils.ValidationError,
'Invalid language code for the translation suggestion counts: '
'%s.' % self.invalid_language_code
):
community_contribution_stats.validate()
class ReviewableSuggestionEmailInfoUnitTests(test_utils.GenericTestBase):
"""Tests for the ReviewableSuggestionEmailInfo class."""
suggestion_type: str = feconf.SUGGESTION_TYPE_ADD_QUESTION
language_code: str = 'en'
suggestion_content: str = 'sample question'
submission_datetime: datetime.datetime = datetime.datetime.utcnow()
def test_initial_object_with_valid_arguments_has_correct_properties(
self
) -> None:
reviewable_suggestion_email_info = (
suggestion_registry.ReviewableSuggestionEmailInfo(
self.suggestion_type, self.language_code,
self.suggestion_content, self.submission_datetime
)
)
self.assertEqual(
reviewable_suggestion_email_info.suggestion_type,
self.suggestion_type)
self.assertEqual(
reviewable_suggestion_email_info.language_code,
self.language_code)
self.assertEqual(
reviewable_suggestion_email_info.suggestion_content,
self.suggestion_content)
self.assertEqual(
reviewable_suggestion_email_info.submission_datetime,
self.submission_datetime)
class TranslationReviewStatsUnitTests(test_utils.GenericTestBase):
"""Tests for the TranslationReviewStats class."""
LANGUAGE_CODE: Final = 'es'
CONTRIBUTOR_USER_ID: Final = 'uid_01234567890123456789012345678912'
TOPIC_ID: Final = 'topic_id'
REVIEWED_TRANSLATIONS_COUNT: Final = 2
REVIEWED_TRANSLATION_WORD_COUNT: Final = 100
ACCEPTED_TRANSLATIONS_COUNT: Final = 1
ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT: Final = 0
ACCEPTED_TRANSLATION_WORD_COUNT: Final = 50
FIRST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
LAST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
def test_create_translation_review_stats(self) -> None:
expected_stats_dict = {
'language_code': self.LANGUAGE_CODE,
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'reviewed_translations_count': self.REVIEWED_TRANSLATIONS_COUNT,
'reviewed_translation_word_count': (
self.REVIEWED_TRANSLATION_WORD_COUNT),
'accepted_translations_count': self.ACCEPTED_TRANSLATIONS_COUNT,
'accepted_translation_word_count': (
self.ACCEPTED_TRANSLATION_WORD_COUNT),
'accepted_translations_with_reviewer_edits_count': (
self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
'first_contribution_date': self.FIRST_CONTRIBUTION_DATE,
'last_contribution_date': self.LAST_CONTRIBUTION_DATE,
}
actual_stats = suggestion_registry.TranslationReviewStats(
self.LANGUAGE_CODE, self.CONTRIBUTOR_USER_ID,
self.TOPIC_ID, self.REVIEWED_TRANSLATIONS_COUNT,
self.REVIEWED_TRANSLATION_WORD_COUNT,
self.ACCEPTED_TRANSLATIONS_COUNT,
self.ACCEPTED_TRANSLATION_WORD_COUNT,
self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_dict(), expected_stats_dict)
class QuestionContributionStatsUnitTests(test_utils.GenericTestBase):
"""Tests for the QuestionContributionStats class."""
CONTRIBUTOR_USER_ID: Final = 'uid_01234567890123456789012345678912'
TOPIC_ID: Final = 'topic_id'
SUBMITTED_QUESTION_COUNT: Final = 2
ACCEPTED_QUESTIONS_COUNT: Final = 1
ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
LAST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
def test_create_question_contribution_stats(self) -> None:
expected_stats_dict = {
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'submitted_questions_count': (
self.SUBMITTED_QUESTION_COUNT),
'accepted_questions_count': (
self.ACCEPTED_QUESTIONS_COUNT),
'accepted_questions_without_reviewer_edits_count': (
self
.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE),
'last_contribution_date': (
self.LAST_CONTRIBUTION_DATE)
}
actual_stats = suggestion_registry.QuestionContributionStats(
self.CONTRIBUTOR_USER_ID, self.TOPIC_ID,
self.SUBMITTED_QUESTION_COUNT, self.ACCEPTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_dict(), expected_stats_dict)
class QuestionReviewStatsUnitTests(test_utils.GenericTestBase):
"""Tests for the QuestionReviewStats class."""
CONTRIBUTOR_USER_ID: Final = 'uid_01234567890123456789012345678912'
TOPIC_ID: Final = 'topic_id'
REVIEWED_QUESTIONS_COUNT: Final = 2
ACCEPTED_QUESTIONS_COUNT: Final = 1
ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
LAST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
def test_create_question_review_stats(self) -> None:
expected_stats_dict = {
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'reviewed_questions_count': self.REVIEWED_QUESTIONS_COUNT,
'accepted_questions_count': (
self.ACCEPTED_QUESTIONS_COUNT),
'accepted_questions_with_reviewer_edits_count': (
self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE),
'last_contribution_date': self.LAST_CONTRIBUTION_DATE
}
actual_stats = suggestion_registry.QuestionReviewStats(
self.CONTRIBUTOR_USER_ID, self.TOPIC_ID,
self.REVIEWED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_dict(), expected_stats_dict)
class ContributorMilestoneEmailInfoUnitTests(test_utils.GenericTestBase):
"""Tests for the ContributorMilestoneEmailInfo class."""
CONTRIBUTOR_USER_ID: Final = 'uid_01234567890123456789012345678912'
CONTRIBUTION_TYPE: Final = 'translation'
CONTRIBUTION_SUBTYPE: Final = 'submission'
LANGUAGE_CODE: Final = 'es'
RANK_NAME: Final = 'Initial Contributor'
def test_create_contribution_milestone_email_info(self) -> None:
actual_info = suggestion_registry.ContributorMilestoneEmailInfo(
self.CONTRIBUTOR_USER_ID, self.CONTRIBUTION_TYPE,
self.CONTRIBUTION_SUBTYPE, self.LANGUAGE_CODE,
self.RANK_NAME
)
self.assertEqual(
actual_info.contributor_user_id, self.CONTRIBUTOR_USER_ID
)
self.assertEqual(
actual_info.contribution_type, self.CONTRIBUTION_TYPE
)
self.assertEqual(
actual_info.contribution_subtype, self.CONTRIBUTION_SUBTYPE
)
self.assertEqual(
actual_info.language_code, self.LANGUAGE_CODE
)
self.assertEqual(
actual_info.rank_name, self.RANK_NAME
)
class ContributorStatsSummaryUnitTests(test_utils.GenericTestBase):
"""Tests for the ContributorStatsSummary class."""
LANGUAGE_CODE: Final = 'es'
CONTRIBUTOR_USER_ID: Final = 'user_01'
TOPIC_ID: Final = 'topic_id'
SUBMITTED_TRANSLATIONS_COUNT: Final = 2
SUBMITTED_TRANSLATION_WORD_COUNT: Final = 100
REJECTED_TRANSLATIONS_COUNT: Final = 0
REJECTED_TRANSLATION_WORD_COUNT: Final = 0
# Timestamp dates in sec since epoch for Mar 19 2021 UTC.
CONTRIBUTION_DATES: Final = {
datetime.date.fromtimestamp(1616173836),
datetime.date.fromtimestamp(1616173837)
}
REVIEWED_TRANSLATIONS_COUNT: Final = 2
REVIEWED_TRANSLATION_WORD_COUNT: Final = 100
ACCEPTED_TRANSLATIONS_COUNT: Final = 1
ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT: Final = 0
ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT: Final = 0
ACCEPTED_TRANSLATION_WORD_COUNT: Final = 50
SUBMITTED_QUESTION_COUNT: Final = 2
ACCEPTED_QUESTIONS_COUNT: Final = 1
ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT: Final = 0
REVIEWED_QUESTIONS_COUNT: Final = 2
ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
LAST_CONTRIBUTION_DATE: Final = datetime.date.fromtimestamp(1616173836)
def test_create_contribution_stats_summary(self) -> None:
expected_translation_contribution_stats = {
'language_code': self.LANGUAGE_CODE,
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'submitted_translations_count': self.SUBMITTED_TRANSLATIONS_COUNT,
'submitted_translation_word_count': (
self.SUBMITTED_TRANSLATION_WORD_COUNT),
'accepted_translations_count': self.ACCEPTED_TRANSLATIONS_COUNT,
'accepted_translations_without_reviewer_edits_count': (
self.ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT),
'accepted_translation_word_count': (
self.ACCEPTED_TRANSLATION_WORD_COUNT),
'rejected_translations_count': self.REJECTED_TRANSLATIONS_COUNT,
'rejected_translation_word_count': (
self.REJECTED_TRANSLATION_WORD_COUNT),
'contribution_dates': self.CONTRIBUTION_DATES
}
expected_translation_review_stats = {
'language_code': self.LANGUAGE_CODE,
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'reviewed_translations_count': self.REVIEWED_TRANSLATIONS_COUNT,
'reviewed_translation_word_count': (
self.REVIEWED_TRANSLATION_WORD_COUNT),
'accepted_translations_count': self.ACCEPTED_TRANSLATIONS_COUNT,
'accepted_translation_word_count': (
self.ACCEPTED_TRANSLATION_WORD_COUNT),
'accepted_translations_with_reviewer_edits_count': (
self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
'first_contribution_date': self.FIRST_CONTRIBUTION_DATE,
'last_contribution_date': self.LAST_CONTRIBUTION_DATE,
}
expected_question_contribution_stats = {
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'submitted_questions_count': (
self.SUBMITTED_QUESTION_COUNT),
'accepted_questions_count': (
self.ACCEPTED_QUESTIONS_COUNT),
'accepted_questions_without_reviewer_edits_count': (
self
.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE),
'last_contribution_date': (
self.LAST_CONTRIBUTION_DATE)
}
expected_question_review_stats = {
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'topic_id': self.TOPIC_ID,
'reviewed_questions_count': self.REVIEWED_QUESTIONS_COUNT,
'accepted_questions_count': (
self.ACCEPTED_QUESTIONS_COUNT),
'accepted_questions_with_reviewer_edits_count': (
self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE),
'last_contribution_date': self.LAST_CONTRIBUTION_DATE
}
expected_contribution_summary = {
'contributor_user_id': self.CONTRIBUTOR_USER_ID,
'translation_contribution_stats': [
expected_translation_contribution_stats],
'question_contribution_stats': [
expected_question_contribution_stats],
'translation_review_stats': [expected_translation_review_stats],
'question_review_stats': [expected_question_review_stats]
}
translation_contribution_stats = (
suggestion_registry).TranslationContributionStats(
self.LANGUAGE_CODE,
self.CONTRIBUTOR_USER_ID,
self.TOPIC_ID,
self.SUBMITTED_TRANSLATIONS_COUNT,
self.SUBMITTED_TRANSLATION_WORD_COUNT,
self.ACCEPTED_TRANSLATIONS_COUNT,
(
self
.ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT
),
self.ACCEPTED_TRANSLATION_WORD_COUNT,
self.REJECTED_TRANSLATIONS_COUNT,
self.REJECTED_TRANSLATION_WORD_COUNT,
self.CONTRIBUTION_DATES
)
translation_review_stats = suggestion_registry.TranslationReviewStats(
self.LANGUAGE_CODE, self.CONTRIBUTOR_USER_ID,
self.TOPIC_ID, self.REVIEWED_TRANSLATIONS_COUNT,
self.REVIEWED_TRANSLATION_WORD_COUNT,
self.ACCEPTED_TRANSLATIONS_COUNT,
self.ACCEPTED_TRANSLATION_WORD_COUNT,
self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
question_contribution_stats = (
suggestion_registry).QuestionContributionStats(
self.CONTRIBUTOR_USER_ID, self.TOPIC_ID,
self.SUBMITTED_QUESTION_COUNT, self.ACCEPTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
question_review_stats = suggestion_registry.QuestionReviewStats(
self.CONTRIBUTOR_USER_ID, self.TOPIC_ID,
self.REVIEWED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
contribution_summary = suggestion_registry.ContributorStatsSummary(
self.CONTRIBUTOR_USER_ID,
[translation_contribution_stats], [question_contribution_stats],
[translation_review_stats], [question_review_stats]
)
self.assertDictEqual(
contribution_summary.to_dict(), expected_contribution_summary
)
class TranslationSubmitterTotalContributionStatsUnitTests(
test_utils.GenericTestBase):
"""Tests for the TranslationSubmitterTotalContributionStats class."""
SUGGESTION_LANGUAGE_CODE: Final = 'es'
TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS: Final = ['topic1', 'topic2']
RECENT_REVIEW_OUTCOMES: Final = ['accepted', 'rejected']
RECENT_PERFORMANCE: Final = 2
OVERALL_ACCURACY: Final = 2.0
SUBMITTED_TRANSLATIONS_COUNT: Final = 2
SUBMITTED_TRANSLATION_WORD_COUNT: Final = 100
ACCEPTED_TRANSLATIONS_COUNT: Final = 1
ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT: Final = 0
ACCEPTED_TRANSLATION_WORD_COUNT: Final = 50
REJECTED_TRANSLATIONS_COUNT: Final = 0
REJECTED_TRANSLATION_WORD_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE = datetime.date.today()
LAST_CONTRIBUTION_DATE = (
datetime.date.today() - datetime.timedelta(25))
user_id: str = 'user_id'
story_id_1: str = 'story_1'
story_id_2: str = 'story_2'
story_id_3: str = 'story_3'
subtopic_id: int = 1
skill_id_1: str = 'skill_1'
skill_id_2: str = 'skill_2'
def test_to_frontend_dict(self) -> None:
auth_id = 'someUser'
username = 'username'
user_settings = user_services.create_new_user(
auth_id, 'user@example.com')
user_services.set_username(user_settings.user_id, username)
topic_id_1 = topic_fetchers.get_new_topic_id()
topic_id_2 = topic_fetchers.get_new_topic_id()
self.save_new_topic(
topic_id_1, self.user_id, name='topic1',
abbreviated_name='name1', url_fragment='name-one',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
self.save_new_topic(
topic_id_2, self.user_id, name='topic2',
abbreviated_name='name2', url_fragment='name-two',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
expected_frontend_dict = {
'language_code': self.SUGGESTION_LANGUAGE_CODE,
'contributor_name': username,
'topic_names': (
self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
'recent_performance': self.RECENT_PERFORMANCE,
'overall_accuracy': self.OVERALL_ACCURACY,
'submitted_translations_count': (
self.SUBMITTED_TRANSLATIONS_COUNT),
'submitted_translation_word_count': (
self.SUBMITTED_TRANSLATION_WORD_COUNT),
'accepted_translations_count': (
self.ACCEPTED_TRANSLATIONS_COUNT),
'accepted_translations_without_reviewer_edits_count': (
self
.ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT),
'accepted_translation_word_count': (
self.ACCEPTED_TRANSLATION_WORD_COUNT),
'rejected_translations_count': (
self.REJECTED_TRANSLATIONS_COUNT),
'rejected_translation_word_count': (
self.REJECTED_TRANSLATION_WORD_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE.strftime('%b %d, %Y')),
'last_contributed_in_days': int(
(datetime.date.today() - self.LAST_CONTRIBUTION_DATE).days)
}
actual_stats = suggestion_registry.TranslationSubmitterTotalContributionStats( # pylint: disable=line-too-long
self.SUGGESTION_LANGUAGE_CODE,
user_settings.user_id,
[topic_id_1, topic_id_2],
self.RECENT_REVIEW_OUTCOMES, self.RECENT_PERFORMANCE,
self.OVERALL_ACCURACY,
self.SUBMITTED_TRANSLATIONS_COUNT,
self.SUBMITTED_TRANSLATION_WORD_COUNT,
self.ACCEPTED_TRANSLATIONS_COUNT,
self.ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT,
self.ACCEPTED_TRANSLATION_WORD_COUNT,
self.REJECTED_TRANSLATIONS_COUNT,
self.REJECTED_TRANSLATION_WORD_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_frontend_dict(), expected_frontend_dict)
class TranslationReviewerTotalContributionStatsUnitTests(
test_utils.GenericTestBase):
"""Tests for the TranslationReviewerTotalContributionStats class."""
SUGGESTION_LANGUAGE_CODE: Final = 'es'
TOPIC_IDS_WITH_TRANSLATION_REVIEWS: Final = ['topic1', 'topic2']
REVIEWED_TRANSLATIONS_COUNT: Final = 2
ACCEPTED_TRANSLATIONS_COUNT: Final = 1
ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT: Final = 0
ACCEPTED_TRANSLATION_WORD_COUNT: Final = 1
REJECTED_TRANSLATIONS_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE = datetime.date.today()
LAST_CONTRIBUTION_DATE = (
datetime.date.today() - datetime.timedelta(25))
user_id: str = 'user_id'
story_id_1: str = 'story_1'
story_id_2: str = 'story_2'
story_id_3: str = 'story_3'
subtopic_id: int = 1
skill_id_1: str = 'skill_1'
skill_id_2: str = 'skill_2'
def test_to_frontend_dict(self) -> None:
auth_id = 'someUser'
username = 'username'
user_settings = user_services.create_new_user(
auth_id, 'user@example.com')
user_services.set_username(user_settings.user_id, username)
topic_id_1 = topic_fetchers.get_new_topic_id()
topic_id_2 = topic_fetchers.get_new_topic_id()
self.save_new_topic(
topic_id_1, self.user_id, name='topic1',
abbreviated_name='name1', url_fragment='name-one',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
self.save_new_topic(
topic_id_2, self.user_id, name='topic2',
abbreviated_name='name2', url_fragment='name-two',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
expected_stats_dict = {
'language_code': self.SUGGESTION_LANGUAGE_CODE,
'contributor_name': username,
'topic_names': (
self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
'reviewed_translations_count': (
self.REVIEWED_TRANSLATIONS_COUNT),
'accepted_translations_count': (
self.ACCEPTED_TRANSLATIONS_COUNT),
'accepted_translations_with_reviewer_edits_count': (
self
.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
'accepted_translation_word_count': (
self.ACCEPTED_TRANSLATION_WORD_COUNT),
'rejected_translations_count': (
self.REJECTED_TRANSLATIONS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE.strftime('%b %d, %Y')),
'last_contributed_in_days': int(
(datetime.date.today() - self.LAST_CONTRIBUTION_DATE).days)
}
actual_stats = suggestion_registry.TranslationReviewerTotalContributionStats( # pylint: disable=line-too-long
self.SUGGESTION_LANGUAGE_CODE,
user_settings.user_id,
[topic_id_1, topic_id_2],
self.REVIEWED_TRANSLATIONS_COUNT,
self.ACCEPTED_TRANSLATIONS_COUNT,
self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT,
self.ACCEPTED_TRANSLATION_WORD_COUNT,
self.REJECTED_TRANSLATIONS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_frontend_dict(), expected_stats_dict)
class QuestionSubmitterTotalContributionStatsUnitTests(
test_utils.GenericTestBase):
"""Tests for the QuestionSubmitterTotalContributionStats class."""
TOPIC_IDS_WITH_QUESTION_SUBMISSIONS: Final = ['topic1', 'topic2']
RECENT_REVIEW_OUTCOMES: Final = ['accepted', 'rejected']
RECENT_PERFORMANCE: Final = 2
OVERALL_ACCURACY: Final = 2.0
SUBMITTED_QUESTIONS_COUNT: Final = 2
SUBMITTED_QUESTION_WORD_COUNT: Final = 100
ACCEPTED_QUESTIONS_COUNT: Final = 1
ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT: Final = 0
ACCEPTED_QUESTION_WORD_COUNT: Final = 50
REJECTED_QUESTIONS_COUNT: Final = 0
REJECTED_QUESTION_WORD_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE = datetime.date.today()
LAST_CONTRIBUTION_DATE = (
datetime.date.today() - datetime.timedelta(25))
user_id: str = 'user_id'
story_id_1: str = 'story_1'
story_id_2: str = 'story_2'
story_id_3: str = 'story_3'
subtopic_id: int = 1
skill_id_1: str = 'skill_1'
skill_id_2: str = 'skill_2'
def test_to_frontend_dict(self) -> None:
auth_id = 'someUser'
username = 'username'
user_settings = user_services.create_new_user(
auth_id, 'user@example.com')
user_services.set_username(user_settings.user_id, username)
topic_id_1 = topic_fetchers.get_new_topic_id()
topic_id_2 = topic_fetchers.get_new_topic_id()
self.save_new_topic(
topic_id_1, self.user_id, name='topic1',
abbreviated_name='name1', url_fragment='name-one',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
self.save_new_topic(
topic_id_2, self.user_id, name='topic2',
abbreviated_name='name2', url_fragment='name-two',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
expected_stats_dict = {
'contributor_name': username,
'topic_names': (
self.TOPIC_IDS_WITH_QUESTION_SUBMISSIONS),
'recent_performance': self.RECENT_PERFORMANCE,
'overall_accuracy': self.OVERALL_ACCURACY,
'submitted_questions_count': (
self.SUBMITTED_QUESTIONS_COUNT),
'accepted_questions_count': (
self.ACCEPTED_QUESTIONS_COUNT),
'accepted_questions_without_reviewer_edits_count': (
self
.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT),
'rejected_questions_count': (
self.REJECTED_QUESTIONS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE.strftime('%b %d, %Y')),
'last_contributed_in_days': int(
(datetime.date.today() - self.LAST_CONTRIBUTION_DATE).days)
}
actual_stats = suggestion_registry.QuestionSubmitterTotalContributionStats( # pylint: disable=line-too-long
user_settings.user_id,
[topic_id_1, topic_id_2],
self.RECENT_REVIEW_OUTCOMES, self.RECENT_PERFORMANCE,
self.OVERALL_ACCURACY,
self.SUBMITTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT,
self.REJECTED_QUESTIONS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_frontend_dict(), expected_stats_dict)
class QuestionReviewerTotalContributionStatsUnitTests(
test_utils.GenericTestBase):
"""Tests for the QuestionReviewerTotalContributionStats class."""
TOPIC_IDS_WITH_QUESTION_REVIEWS: Final = ['topic1', 'topic2']
REVIEWED_QUESTIONS_COUNT: Final = 2
ACCEPTED_QUESTIONS_COUNT: Final = 1
ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT: Final = 0
REJECTED_QUESTIONS_COUNT: Final = 0
FIRST_CONTRIBUTION_DATE = datetime.date.today()
LAST_CONTRIBUTION_DATE = (
datetime.date.today() - datetime.timedelta(25))
user_id: str = 'user_id'
story_id_1: str = 'story_1'
story_id_2: str = 'story_2'
story_id_3: str = 'story_3'
subtopic_id: int = 1
skill_id_1: str = 'skill_1'
skill_id_2: str = 'skill_2'
def test_to_frontend_dict(self) -> None:
auth_id = 'someUser'
username = 'username'
user_settings = user_services.create_new_user(
auth_id, 'user@example.com')
user_services.set_username(user_settings.user_id, username)
topic_id_1 = topic_fetchers.get_new_topic_id()
topic_id_2 = topic_fetchers.get_new_topic_id()
self.save_new_topic(
topic_id_1, self.user_id, name='topic1',
abbreviated_name='name1', url_fragment='name-one',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
self.save_new_topic(
topic_id_2, self.user_id, name='topic2',
abbreviated_name='name2', url_fragment='name-two',
description='Description',
canonical_story_ids=[self.story_id_1, self.story_id_2],
additional_story_ids=[self.story_id_3],
uncategorized_skill_ids=[self.skill_id_1, self.skill_id_2],
subtopics=[], next_subtopic_id=1)
expected_stats_dict = {
'contributor_name': username,
'topic_names': (
self.TOPIC_IDS_WITH_QUESTION_REVIEWS),
'reviewed_questions_count': (
self.REVIEWED_QUESTIONS_COUNT),
'accepted_questions_count': (
self.ACCEPTED_QUESTIONS_COUNT),
'accepted_questions_with_reviewer_edits_count': (
self
.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT),
'rejected_questions_count': (
self.REJECTED_QUESTIONS_COUNT),
'first_contribution_date': (
self.FIRST_CONTRIBUTION_DATE.strftime('%b %d, %Y')),
'last_contributed_in_days': int(
(datetime.date.today() - self.LAST_CONTRIBUTION_DATE).days)
}
actual_stats = suggestion_registry.QuestionReviewerTotalContributionStats( # pylint: disable=line-too-long
user_settings.user_id,
[topic_id_1, topic_id_2],
self.REVIEWED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_COUNT,
self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT,
self.REJECTED_QUESTIONS_COUNT,
self.FIRST_CONTRIBUTION_DATE, self.LAST_CONTRIBUTION_DATE
)
self.assertDictEqual(
actual_stats.to_frontend_dict(), expected_stats_dict)