core/storage/suggestion/gae_models_test.py

Summary

Maintainability
F
2 wks
Test Coverage
# coding: utf-8
#
# 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 the suggestion gae_models."""

from __future__ import annotations

import datetime

from core import feconf
from core import utils
from core.constants import constants
from core.platform import models
from core.tests import test_utils
from typing import Dict, Final, List, Mapping

MYPY = False
if MYPY: # pragma: no cover
    # Here, change domain is imported only for type checking.
    from core.domain import change_domain  # pylint: disable=invalid-import # isort:skip
    from mypy_imports import base_models
    from mypy_imports import suggestion_models

(base_models, suggestion_models, user_models) = models.Registry.import_models([
    models.Names.BASE_MODEL, models.Names.SUGGESTION, models.Names.USER
])


class SuggestionModelUnitTests(test_utils.GenericTestBase):
    """Tests for the suggestionModel class."""

    score_category: str = (
        suggestion_models.SCORE_TYPE_TRANSLATION +
        suggestion_models.SCORE_CATEGORY_DELIMITER + 'English')

    topic_name = 'topic'
    target_id = 'exp1'
    target_version_at_submission = 1
    change_cmd: Mapping[
        str, change_domain.AcceptableChangeDictTypes
    ] = {}
    # Language code that would normally be derived from the change_cmd.
    translation_language_code = 'en'
    # Language code that would normally be derived from the question_dict in
    # the change_cmd.
    question_language_code = 'en'
    mocked_datetime_utcnow = datetime.datetime(2020, 6, 15, 5)

    def setUp(self) -> None:
        super().setUp()
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_1',
            'reviewer_1', self.change_cmd, self.score_category,
            'exploration.exp1.thread_1', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_ACCEPTED, 'author_2',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_2', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_ACCEPTED, 'author_2',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_3', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_2',
            'reviewer_3', self.change_cmd, self.score_category,
            'exploration.exp1.thread_4', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_5', None)

    def test_get_in_review_translation_suggestions_by_exp_id(self) -> None:
        model = suggestion_models.GeneralSuggestionModel
        self.assertEqual(
            model.get_in_review_translation_suggestions_by_exp_id(
                self.target_id),
            []
        )

        suggestion_id = 'exploration.exp1.thread_6'
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_1',
            'reviewer_3', self.change_cmd, self.score_category,
            suggestion_id, 'en')

        created_suggestion_model = model.get_by_id(suggestion_id)
        self.assertEqual(
            model.get_in_review_translation_suggestions_by_exp_id(
                self.target_id),
            [created_suggestion_model]
        )

    def test_get_all_in_review_translation_suggestions_by_exp_ids(
            self) -> None:
        model = suggestion_models.GeneralSuggestionModel
        self.assertEqual(
            model.get_in_review_translation_suggestions_by_exp_ids(
                [self.target_id], 'en'),
            []
        )
        suggestion_id = 'exploration.exp1.thread_6'
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_1',
            'reviewer_3', self.change_cmd, self.score_category,
            suggestion_id, 'en')

        created_suggestion_model = model.get_by_id(suggestion_id)
        self.assertEqual(
            model.get_in_review_translation_suggestions_by_exp_ids(
                [self.target_id], 'en'),
            [created_suggestion_model]
        )

    def test_get_all_user_created_suggestions_of_given_suggestion_type(
            self) -> None:
        model = suggestion_models.GeneralSuggestionModel
        expected_suggestion_model = model.get_by_id(
            'exploration.exp1.thread_1')
        self.assertEqual(
            model.get_user_created_suggestions_of_suggestion_type(
                feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT, 'author_1'),
            [expected_suggestion_model]
        )

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            suggestion_models.GeneralSuggestionModel.get_deletion_policy(),
            base_models.DELETION_POLICY.LOCALLY_PSEUDONYMIZE)

    def test_has_reference_to_user_id(self) -> None:
        self.assertTrue(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('author_1')
        )
        self.assertTrue(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('author_2')
        )
        self.assertTrue(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('author_3')
        )
        self.assertTrue(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('reviewer_1')
        )
        self.assertTrue(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('reviewer_2')
        )
        self.assertTrue(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('reviewer_3')
        )
        self.assertFalse(
            suggestion_models.GeneralSuggestionModel
            .has_reference_to_user_id('id_x')
        )

    def test_score_type_contains_delimiter(self) -> None:
        for score_type in suggestion_models.SCORE_TYPE_CHOICES:
            self.assertTrue(
                suggestion_models.SCORE_CATEGORY_DELIMITER not in score_type)

    def test_get_translation_suggestions_submitted_for_given_date_range(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_ACCEPTED, 'test_author',
            'reviewer_1', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', 'hi')
        to_date = datetime.datetime.now()
        from_date = to_date - datetime.timedelta(days=1)

        suggestions = (
            suggestion_models.GeneralSuggestionModel
                .get_translation_suggestions_submitted_within_given_dates(
                    from_date, to_date, 'test_author', 'hi'))

        self.assertEqual(len(suggestions), 1)

    def test_get_question_suggestions_submitted_for_given_date_range(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_ACCEPTED, 'test_author',
            'reviewer_1', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', 'hi')
        to_date = datetime.datetime.now()
        from_date = to_date - datetime.timedelta(days=1)

        suggestions = (
            suggestion_models.GeneralSuggestionModel
                .get_question_suggestions_submitted_within_given_dates(
                    from_date, to_date, 'test_author'))

        self.assertEqual(len(suggestions), 1)

    def test_create_new_object_succesfully(self) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_3', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', None)

        suggestion_id = 'exploration.exp1.thread_6'

        observed_suggestion_model = (
            suggestion_models.GeneralSuggestionModel.get_by_id(suggestion_id))

        self.assertEqual(
            observed_suggestion_model.suggestion_type,
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT)
        self.assertEqual(
            observed_suggestion_model.target_type,
            feconf.ENTITY_TYPE_EXPLORATION)
        self.assertEqual(
            observed_suggestion_model.target_id, self.target_id)
        self.assertEqual(
            observed_suggestion_model.target_version_at_submission,
            self.target_version_at_submission)
        self.assertEqual(
            observed_suggestion_model.status,
            suggestion_models.STATUS_IN_REVIEW)
        self.assertEqual(observed_suggestion_model.author_id, 'author_3')
        self.assertEqual(
            observed_suggestion_model.final_reviewer_id, 'reviewer_3')
        self.assertEqual(
            observed_suggestion_model.score_category, self.score_category)
        self.assertEqual(observed_suggestion_model.change_cmd, self.change_cmd)

    def test_create_suggestion_fails_if_id_collides_with_existing_one(
        self
    ) -> None:
        with self.assertRaisesRegex(
            Exception, 'There is already a suggestion with the given id: '
                       'exploration.exp1.thread_1'):
            suggestion_models.GeneralSuggestionModel.create(
                feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
                feconf.ENTITY_TYPE_EXPLORATION,
                self.target_id, self.target_version_at_submission,
                suggestion_models.STATUS_IN_REVIEW, 'author_3',
                'reviewer_3', self.change_cmd,
                self.score_category, 'exploration.exp1.thread_1', None)

    def test_get_suggestions_by_type(self) -> None:
        queries = [(
            'suggestion_type',
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT)]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 5)
        queries = [('suggestion_type', 'invalid_suggestion_type')]

        with self.assertRaisesRegex(
            Exception, 'Value \'invalid_suggestion_type\' for property'
                       ' suggestion_type is not an allowed choice'):
            suggestion_models.GeneralSuggestionModel.query_suggestions(queries)

    def test_get_suggestion_by_author(self) -> None:
        queries = [('author_id', 'author_1')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)
        queries = [('author_id', 'author_2')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 3)
        queries = [('author_id', 'author_3')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)
        queries = [('author_id', 'author_invalid')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 0)

    def test_get_suggestion_by_reviewer(self) -> None:
        queries = [('final_reviewer_id', 'reviewer_1')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)
        queries = [('final_reviewer_id', 'reviewer_2')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 3)
        queries = [('final_reviewer_id', 'reviewer_3')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)
        queries = [('final_reviewer_id', 'reviewer_invalid')]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 0)

    def test_get_suggestions_by_status(self) -> None:
        queries = [('status', suggestion_models.STATUS_IN_REVIEW)]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)
        queries = [('status', suggestion_models.STATUS_REJECTED)]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 2)
        queries = [('status', suggestion_models.STATUS_ACCEPTED)]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 2)

    def test_get_suggestions_by_target_id(self) -> None:
        queries = [
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', self.target_id)
        ]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 5)
        queries = [
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', 'exp_invalid')
        ]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 0)

    def test_query_suggestions(self) -> None:
        queries = [
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', self.target_id)
        ]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 5)

        queries = [
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', self.target_id),
            ('author_id', 'author_2')
        ]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 3)

        queries = [
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', self.target_id),
            ('author_id', 'author_2'),
            ('status', suggestion_models.STATUS_ACCEPTED)
        ]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 2)

        queries = [
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', self.target_id),
            ('invalid_field', 'value')
        ]
        with self.assertRaisesRegex(
            Exception, 'Not allowed to query on field invalid_field'):
            suggestion_models.GeneralSuggestionModel.query_suggestions(queries)

        queries = [
            (
                'suggestion_type',
                feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT),
            ('target_type', feconf.ENTITY_TYPE_EXPLORATION),
            ('target_id', self.target_id),
            ('status', suggestion_models.STATUS_IN_REVIEW),
            ('author_id', 'author_1'),
            ('final_reviewer_id', 'reviewer_1'),
            ('score_category', self.score_category)
        ]
        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)

    def test_query_suggestions_by_language(self) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)

        queries = [('language_code', self.translation_language_code)]

        self.assertEqual(
            len(suggestion_models.GeneralSuggestionModel.query_suggestions(
                queries)), 1)

    def test_get_in_review_translation_suggestions(self) -> None:
        # Create two in-review translation suggestions.
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)
        # Create accepted and rejected suggestions that should not be returned.
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_ACCEPTED, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_8', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_9', self.translation_language_code)

        suggestions = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions(
                'exp1', [self.translation_language_code]))

        self.assertEqual(len(suggestions), 2)
        self.assertEqual(suggestions[0].target_id, 'exp1')
        self.assertEqual(
            suggestions[0].suggestion_type,
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT)
        self.assertEqual(
            suggestions[0].status,
            suggestion_models.STATUS_IN_REVIEW)
        self.assertEqual(suggestions[1].target_id, 'exp1')
        self.assertEqual(
            suggestions[1].suggestion_type,
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT)
        self.assertEqual(
            suggestions[1].status,
            suggestion_models.STATUS_IN_REVIEW)

    def test_get_reviewable_translation_suggestions(
        self
    ) -> None:
        suggestion_1_id = 'exploration.exp1.thread_6'
        suggestion_2_id = 'exploration.exp1.thread_7'
        suggestion_3_id = 'exploration.exp1.thread_8'
        user_id = 'author1'
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_1_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_2_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, user_id,
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_3_id, self.translation_language_code)

        results, _ = (
            suggestion_models.GeneralSuggestionModel
            .get_reviewable_translation_suggestions(
                user_id=user_id,
                language_code=self.translation_language_code,
                exp_id='exp1'))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), 2)

    def test_get_translation_suggestions_in_review_with_valid_exp(self) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)

        suggestions = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_in_review_with_exp_id(
                'exp1', self.translation_language_code))

        self.assertEqual(len(suggestions), 2)
        self.assertEqual(suggestions[0].target_id, 'exp1')
        self.assertEqual(
            suggestions[0].suggestion_type,
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT)
        self.assertEqual(
            suggestions[0].status,
            suggestion_models.STATUS_IN_REVIEW)
        self.assertEqual(suggestions[1].target_id, 'exp1')

    def test_get_translation_suggestions_in_review_with_exp_ids_by_offset(
        self
    ) -> None:
        limit = 1
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)

        suggestions, offset_1 = (
            suggestion_models
                .GeneralSuggestionModel
                .get_in_review_translation_suggestions_with_exp_ids_by_offset(
                    limit, 0, 'author_4', None,
                    [self.translation_language_code], ['exp1']))

        self.assertEqual(len(suggestions), 1)
        self.assertEqual(suggestions[0].target_id, 'exp1')
        self.assertEqual(offset_1, 1)
        self.assertEqual(
            suggestions[0].suggestion_type,
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT)
        self.assertEqual(
            suggestions[0].status,
            suggestion_models.STATUS_IN_REVIEW)

    def test_get_translation_suggestions_in_review_with_exp_ids_by_offset_sorted( # pylint: disable=line-too-long
        self
    ) -> None:
        suggestion_1_id = 'exploration.exp1.thread_6'
        suggestion_2_id = 'exploration.exp1.thread_7'
        suggestion_3_id = 'exploration.exp1.thread_8'
        user_id = 'author1'
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_1_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_2_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, user_id,
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_3_id, self.translation_language_code)

        sorted_results, offset_1 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_with_exp_ids_by_offset(
                limit=1,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code],
                exp_ids=['exp1']))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(offset_1, 2)

        sorted_results, offset_2 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_with_exp_ids_by_offset(
                limit=2,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code],
                exp_ids=['exp1']))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset_2, 3)

        sorted_results, offset_3 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_with_exp_ids_by_offset(
                limit=10,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code],
                exp_ids=['exp1']))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset_3, 3)

        sorted_results, offset_4 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_with_exp_ids_by_offset(
                limit=None,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code],
                exp_ids=['exp1']))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset_4, 3)

    def test_get_in_review_translation_suggestions_by_offset(self) -> None:
        suggestion_1_id = 'exploration.exp1.thread_6'
        suggestion_2_id = 'exploration.exp1.thread_7'
        user_id = 'author1'
        limit = 1
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_1_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_2_id, self.translation_language_code)

        results, offset_1 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=limit,
                offset=0,
                user_id=user_id,
                sort_key=None,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), limit)
        self.assertEqual(results[0].id, suggestion_1_id)
        self.assertEqual(offset_1, 1)

        results, offset_2 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=limit,
                offset=offset_1,
                user_id=user_id,
                sort_key=None,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), limit)
        self.assertEqual(results[0].id, suggestion_2_id)
        self.assertEqual(offset_2, 2)

        results, offset_3 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=limit,
                offset=offset_2,
                user_id=user_id,
                sort_key=None,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), 0)
        self.assertEqual(offset_3, 2)

    def test_get_in_review_translation_suggestions_by_offset_no_limit(
        self
    ) -> None:
        suggestion_1_id = 'exploration.exp1.thread_6'
        suggestion_2_id = 'exploration.exp1.thread_7'
        user_id = 'author1'
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_1_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_2_id, self.translation_language_code)

        results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=None,
                offset=0,
                user_id=user_id,
                sort_key=None,
                language_codes=[self.translation_language_code]))

        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), 2)
        self.assertEqual(results[0].id, suggestion_1_id)
        self.assertEqual(results[1].id, suggestion_2_id)
        self.assertEqual(offset, 2)

    def test_get_in_review_translation_suggestions_by_offset_sorted(
        self
    ) -> None:
        suggestion_1_id = 'exploration.exp1.thread_6'
        suggestion_2_id = 'exploration.exp1.thread_7'
        suggestion_3_id = 'exploration.exp1.thread_8'
        user_id = 'author1'
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_1_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_2_id, self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, user_id,
            'reviewer_2', self.change_cmd, self.score_category,
            suggestion_3_id, self.translation_language_code)

        sorted_results, offset_1 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=1,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(offset_1, 2)

        sorted_results, offset_2 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=2,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset_2, 3)

        sorted_results, offset_3 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=10,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset_3, 3)

        sorted_results, offset_4 = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_translation_suggestions_by_offset(
                limit=None,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                language_codes=[self.translation_language_code]))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset_4, 3)

    def test_get_in_review_question_suggestions_by_offset(self) -> None:
        suggestion_1_id = 'skill1.thread1'
        suggestion_2_id = 'skill1.thread2'
        suggestion_3_id = 'skill2.thread3'
        user_id = 'author1'
        limit = 1
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category1',
            suggestion_1_id, self.question_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, 'category1',
            suggestion_2_id, self.question_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_3', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author1',
            'reviewer_2', self.change_cmd, 'category1',
            suggestion_3_id, self.question_language_code)

        results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=limit,
                offset=0,
                user_id=user_id,
                sort_key=None,
                skill_ids=None))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), limit)
        self.assertEqual(results[0].id, suggestion_1_id)
        self.assertEqual(offset, 1)
        prev_offset = offset

        results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=limit,
                offset=prev_offset,
                user_id=user_id,
                sort_key=None,
                skill_ids=None))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), limit)
        self.assertEqual(results[0].id, suggestion_2_id)
        self.assertEqual(offset, 2)
        prev_offset = offset

        results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=limit,
                offset=prev_offset,
                user_id=user_id,
                sort_key=None,
                skill_ids=None))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), 0)
        self.assertEqual(offset, 2)

        sorted_results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=1,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                skill_ids=None))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(offset, 2)

        sorted_results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=2,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                skill_ids=None))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset, 3)

        sorted_results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=10,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                skill_ids=None))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, suggestion_2_id)
        self.assertEqual(sorted_results[1].id, suggestion_1_id)
        self.assertEqual(offset, 3)

        sorted_results, offset = (
            suggestion_models.GeneralSuggestionModel
            .get_in_review_question_suggestions_by_offset(
                limit=10,
                offset=0,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                skill_ids=['skill_1', 'skill_3']))
        # Ruling out the possibility of None for mypy type checking.
        assert sorted_results is not None
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, suggestion_1_id)
        self.assertEqual(offset, 3)

        with self.assertRaisesRegex(
            RuntimeError,
            'skill_ids list can\'t be empty'):
            (
                suggestion_models.GeneralSuggestionModel
                .get_in_review_question_suggestions_by_offset(
                    limit=10,
                    offset=0,
                    user_id=user_id,
                    sort_key=constants.SUGGESTIONS_SORT_KEY_DATE,
                    skill_ids=[]))

    def test_user_created_suggestions_by_offset(self) -> None:
        authored_translation_suggestion_id = 'exploration.exp1.thread_6'
        non_authored_translation_suggestion_id = 'exploration.exp1.thread_7'
        authored_question_suggestion_id = 'skill1.thread1'
        user_id = 'author1'
        limit = 1
        # User created translation suggestion.
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, user_id,
            'reviewer_2', self.change_cmd, self.score_category,
            authored_translation_suggestion_id, self.translation_language_code)
        # Translation suggestion created by a different user.
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            non_authored_translation_suggestion_id,
            self.translation_language_code)
        # User created question suggestion.
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, user_id,
            'reviewer_2', self.change_cmd, 'category1',
            authored_question_suggestion_id, self.question_language_code)

        results, translation_suggestion_offset = (
            suggestion_models.GeneralSuggestionModel
            .get_user_created_suggestions_by_offset(
                limit=limit,
                offset=0,
                suggestion_type=feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
                user_id=user_id,
                sort_key=constants.SUGGESTIONS_SORT_KEY_DATE))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), limit)
        self.assertEqual(results[0].id, authored_translation_suggestion_id)
        self.assertEqual(translation_suggestion_offset, 1)

        results, question_suggestion_offset = (
            suggestion_models.GeneralSuggestionModel
            .get_user_created_suggestions_by_offset(
                limit=limit,
                offset=0,
                suggestion_type=feconf.SUGGESTION_TYPE_ADD_QUESTION,
                user_id=user_id,
                sort_key=None))
        # Ruling out the possibility of None for mypy type checking.
        assert results is not None
        self.assertEqual(len(results), limit)
        self.assertEqual(results[0].id, authored_question_suggestion_id)
        self.assertEqual(question_suggestion_offset, 1)

    def test_get_translation_suggestions_in_review_with_exp_id_with_invalid_exp(
            self
    ) -> None:
        suggestions = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_in_review_with_exp_id(
                'invalid_exp', 'hi'))
        self.assertEqual(len(suggestions), 0)

    def test_get_translation_suggestion_ids_with_exp_ids_with_one_exp(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)

        # Assert that there is one translation suggestion with the given
        # exploration id found.
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestion_ids_with_exp_ids(
                ['exp1'])), 1)

    def test_get_exp_translation_suggestions_in_review_returns_limited_values(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)

        with self.swap(feconf, 'DEFAULT_SUGGESTION_QUERY_LIMIT', 1):
            suggestions = (
                suggestion_models.GeneralSuggestionModel
                .get_translation_suggestions_in_review_with_exp_id(
                    'exp1', self.translation_language_code))

        self.assertEqual(len(suggestions), 1)

    def test_get_exp_translation_suggestions_in_review_for_resolved_suggestion_returns_no_items( # pylint: disable=line-too-long
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_ACCEPTED, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)

        suggestions = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_in_review_with_exp_id(
                'exp1', self.translation_language_code))

        self.assertEqual(len(suggestions), 0)

    def test_get_exp_translation_suggestions_in_review_for_non_translation_suggestion_returns_no_items( # pylint: disable=line-too-long
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_6', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)

        suggestions = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_in_review_with_exp_id(
                'exp1', self.translation_language_code))

        self.assertEqual(len(suggestions), 0)

    def test_get_exp_translation_suggestions_in_review_for_different_language_code_returns_no_items( # pylint: disable=line-too-long
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_4',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', 'hi')

        suggestions = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_in_review_with_exp_id('exp1', 'pt'))

        self.assertEqual(len(suggestions), 0)

    def test_get_translation_suggestion_ids_with_exp_ids_with_multiple_exps(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_7', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp3', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_8', self.translation_language_code)

        # Assert that there are two translation suggestions with the given
        # exploration ids found.
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestion_ids_with_exp_ids(
                ['exp2', 'exp3'])), 2)

    def test_get_translation_suggestion_ids_with_exp_ids_with_invalid_exp(
        self
    ) -> None:
        # Assert that there are no translation suggestions with an invalid
        # exploration id found.
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestion_ids_with_exp_ids(
                ['invalid_exp'])), 0)

    def test_get_translation_suggestion_ids_with_exp_ids_past_default_query(
            self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp4', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_9', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp5', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread_10', self.translation_language_code)

        with self.swap(feconf, 'DEFAULT_SUGGESTION_QUERY_LIMIT', 1):
            suggestion_model_results = (
                suggestion_models
                .GeneralSuggestionModel
                .get_translation_suggestion_ids_with_exp_ids(
                    ['exp4', 'exp5'])
            )

        # Assert that there are two translation suggestions with the given
        # exploration ids found. There should be two fetch_page calls.
        self.assertEqual(len(suggestion_model_results), 2)

    def test_get_all_stale_suggestion_ids(self) -> None:
        with self.swap(
            suggestion_models, 'THRESHOLD_TIME_BEFORE_ACCEPT_IN_MSECS', 0):
            self.assertEqual(len(
                suggestion_models.GeneralSuggestionModel
                .get_all_stale_suggestion_ids()), 1)

        with self.swap(
            suggestion_models, 'THRESHOLD_TIME_BEFORE_ACCEPT_IN_MSECS',
            7 * 24 * 60 * 60 * 1000):
            self.assertEqual(len(
                suggestion_models.GeneralSuggestionModel
                .get_all_stale_suggestion_ids()), 0)

    def test_get__suggestions_waiting_too_long_raises_if_suggestion_types_empty(
        self
    ) -> None:
        with self.swap(
            feconf, 'CONTRIBUTOR_DASHBOARD_SUGGESTION_TYPES', []):
            with self.assertRaisesRegex(
                Exception,
                'Expected the suggestion types offered on the Contributor '
                'Dashboard to be nonempty.'):
                (
                    suggestion_models.GeneralSuggestionModel
                    .get_suggestions_waiting_too_long_for_review()
                )

    def test_get_new_suggestions_waiting_for_review(self) -> None:
        suggestion_type = feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT
        max_suggestions = 1
        creation_time = datetime.datetime(2020, 6, 14, 5)
        creation_time_in_millisecs = int(creation_time.timestamp() * 1000)
        mock_value = creation_time_in_millisecs

        mock_get_current_time_in_millisecs = lambda: mock_value

        with self.swap(
            utils, 'get_current_time_in_millisecs',
            mock_get_current_time_in_millisecs):
            with self.mock_datetime_utcnow(self.mocked_datetime_utcnow):
                suggestion_models.GeneralSuggestionModel.create(
                    suggestion_type, feconf.ENTITY_TYPE_EXPLORATION,
                    self.target_id, self.target_version_at_submission,
                    suggestion_models.STATUS_IN_REVIEW, 'author_3',
                    'reviewer_2', self.change_cmd, self.score_category,
                    's.thread1', None)

            with self.mock_datetime_utcnow(self.mocked_datetime_utcnow):
                results = (
                    suggestion_models.GeneralSuggestionModel.
                        get_new_suggestions_waiting_for_review())

        self.assertEqual(len(results), max_suggestions)

    def test_get_suggestions_waiting_too_long_if_not_contributor_suggestion(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread1', None)
        # This mocked list cannot be empty because then the query will fail.
        mocked_contributor_dashboard_suggestion_types = [
            feconf.SUGGESTION_TYPE_ADD_QUESTION]

        with self.swap(
            feconf, 'CONTRIBUTOR_DASHBOARD_SUGGESTION_TYPES',
            mocked_contributor_dashboard_suggestion_types):
            with self.swap(
                suggestion_models,
                'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS', 0):
                suggestions_waiting_too_long_for_review = (
                    suggestion_models.GeneralSuggestionModel
                    .get_suggestions_waiting_too_long_for_review()
                )

        self.assertEqual(len(suggestions_waiting_too_long_for_review), 0)

    def test_get_suggestions_waiting_too_long_returns_empty_if_neg_timedelta(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread1', self.translation_language_code)

        # Make sure the threshold is nonzero.
        with self.swap(
            suggestion_models,
            'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS', 1):
            suggestions_waiting_too_long_for_review = (
                suggestion_models.GeneralSuggestionModel
                .get_suggestions_waiting_too_long_for_review()
            )

        self.assertEqual(len(suggestions_waiting_too_long_for_review), 0)

    def test_get_suggestions_waiting_too_long_if_suggestions_waited_less_limit(
        self
    ) -> None:
        with self.mock_datetime_utcnow(self.mocked_datetime_utcnow):
            suggestion_models.GeneralSuggestionModel.create(
                feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
                feconf.ENTITY_TYPE_EXPLORATION,
                'exp1', self.target_version_at_submission,
                suggestion_models.STATUS_IN_REVIEW, 'author_3',
                'reviewer_2', self.change_cmd, self.score_category,
                'exploration.exp1.thread1', self.translation_language_code)
        mocked_threshold_review_wait_time_in_days = 2
        mocked_datetime_less_than_review_wait_time_threshold = (
            self.mocked_datetime_utcnow + datetime.timedelta(days=1))

        with self.mock_datetime_utcnow(
            mocked_datetime_less_than_review_wait_time_threshold):
            with self.swap(
                suggestion_models,
                'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS',
                mocked_threshold_review_wait_time_in_days):
                suggestions_waiting_too_long_for_review = (
                    suggestion_models.GeneralSuggestionModel
                    .get_suggestions_waiting_too_long_for_review()
                )

        self.assertEqual(len(suggestions_waiting_too_long_for_review), 0)

    def test_get_suggestions_waiting_too_long_if_suggestion_waited_limit(
        self
    ) -> None:
        with self.mock_datetime_utcnow(self.mocked_datetime_utcnow):
            suggestion_models.GeneralSuggestionModel.create(
                feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
                feconf.ENTITY_TYPE_EXPLORATION,
                'exp1', self.target_version_at_submission,
                suggestion_models.STATUS_IN_REVIEW, 'author_3',
                'reviewer_2', self.change_cmd, self.score_category,
                'exploration.exp1.thread1', self.translation_language_code)
        mocked_threshold_review_wait_time_in_days = 2
        mocked_datetime_eq_review_wait_time_threshold = (
            self.mocked_datetime_utcnow + datetime.timedelta(
                days=mocked_threshold_review_wait_time_in_days))

        with self.mock_datetime_utcnow(
            mocked_datetime_eq_review_wait_time_threshold):
            with self.swap(
                suggestion_models,
                'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS',
                mocked_threshold_review_wait_time_in_days):
                suggestions_waiting_too_long_for_review = (
                    suggestion_models.GeneralSuggestionModel
                    .get_suggestions_waiting_too_long_for_review()
                )

        self.assertEqual(len(suggestions_waiting_too_long_for_review), 0)

    def test_get_suggestions_waiting_too_long_if_suggestion_waited_past_limit(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread1', self.translation_language_code)

        with self.swap(
            suggestion_models,
            'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS', 0):
            suggestions_waiting_too_long_for_review = (
                suggestion_models.GeneralSuggestionModel
                .get_suggestions_waiting_too_long_for_review()
            )

        self.assertEqual(len(suggestions_waiting_too_long_for_review), 1)

    def test_get_suggestions_waiting_too_long_with_diff_review_wait_times(
        self
    ) -> None:
        with self.mock_datetime_utcnow(self.mocked_datetime_utcnow):
            suggestion_models.GeneralSuggestionModel.create(
                feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
                feconf.ENTITY_TYPE_EXPLORATION,
                'exp1', self.target_version_at_submission,
                suggestion_models.STATUS_IN_REVIEW, 'author_3',
                'reviewer_2', self.change_cmd, self.score_category,
                'exploration.exp1.thread1', self.translation_language_code)
        with self.mock_datetime_utcnow(
            self.mocked_datetime_utcnow + datetime.timedelta(days=2)):
            suggestion_models.GeneralSuggestionModel.create(
                feconf.SUGGESTION_TYPE_ADD_QUESTION,
                feconf.ENTITY_TYPE_SKILL,
                'skill_1', self.target_version_at_submission,
                suggestion_models.STATUS_IN_REVIEW, 'author_3',
                'reviewer_2', self.change_cmd, 'category1',
                'skill1.thread1', self.question_language_code)
        mocked_threshold_review_wait_time_in_days = 3
        mocked_datetime_past_review_wait_time_threshold = (
            self.mocked_datetime_utcnow + datetime.timedelta(days=4))

        with self.mock_datetime_utcnow(
            mocked_datetime_past_review_wait_time_threshold):
            with self.swap(
                suggestion_models,
                'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS',
                mocked_threshold_review_wait_time_in_days):
                suggestions_waiting_too_long_for_review = (
                    suggestion_models.GeneralSuggestionModel
                    .get_suggestions_waiting_too_long_for_review())

        # The question suggestion was created 2 days after the translation
        # suggestion, so it has only waited 1 day for a review, which is less
        # than 3, the mocked review wait time threshold. Therefore, only the
        # translation suggestion has waited too long for a review.
        self.assertEqual(len(suggestions_waiting_too_long_for_review), 1)
        self.assertEqual(
            suggestions_waiting_too_long_for_review[0].id,
            'exploration.exp1.thread1')

    def test_get_suggestions_waiting_too_long_returns_in_correct_wait_order(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread1', 'fr')
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp2.thread1', 'en')
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp3', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp3.thread1', 'hi')

        with self.swap(
            suggestion_models,
            'SUGGESTION_REVIEW_WAIT_TIME_THRESHOLD_IN_DAYS', 0):
            suggestions_waiting_too_long_for_review = (
                suggestion_models.GeneralSuggestionModel
                .get_suggestions_waiting_too_long_for_review()
            )

        self.assertEqual(len(suggestions_waiting_too_long_for_review), 3)
        # Assert that the order of the returned suggestion models represents
        # the suggestions sorted in descending order, based on how long each
        # suggestion has been waiting for review.
        self.assertEqual(
            suggestions_waiting_too_long_for_review[0].id,
            'exploration.exp1.thread1')
        self.assertEqual(
            suggestions_waiting_too_long_for_review[1].id,
            'exploration.exp2.thread1')
        self.assertEqual(
            suggestions_waiting_too_long_for_review[2].id,
            'exploration.exp3.thread1')

    def test_get_in_review_suggestions_in_score_categories(self) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category1',
            'exploration.exp1.thread_6', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_2',
            'reviewer_2', self.change_cmd, 'category2',
            'exploration.exp1.thread_7', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_2',
            'reviewer_2', self.change_cmd, 'category3',
            'exploration.exp1.thread_8', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_2',
            'reviewer_2', self.change_cmd, 'category1',
            'exploration.exp1.thread_9', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category2',
            'exploration.exp1.thread_10', None)

        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_in_review_suggestions_in_score_categories(
                ['category1'], 'author_3')), 0)
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_in_review_suggestions_in_score_categories(
                ['category1'], 'author_2')), 1)
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_in_review_suggestions_in_score_categories(
                ['category2'], 'author_2')), 1)
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_in_review_suggestions_in_score_categories(
                ['category1', 'category2'], 'author_3')), 1)
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_in_review_suggestions_in_score_categories(
                ['category1', 'category2', 'category3'], 'author_1')), 4)
        self.assertEqual(len(
            suggestion_models.GeneralSuggestionModel
            .get_in_review_suggestions_in_score_categories(
                ['category1', 'category_invalid'], 'author_2')), 1)
        with self.assertRaisesRegex(
            Exception, 'Received empty list of score categories'):
            self.assertEqual(len(
                suggestion_models.GeneralSuggestionModel
                .get_in_review_suggestions_in_score_categories(
                    [], 'author_1')), 0)

    def test_get_all_score_categories(self) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category1',
            'exploration.exp1.thread_11', None)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            self.target_id, self.target_version_at_submission,
            suggestion_models.STATUS_REJECTED, 'author_3',
            'reviewer_2', self.change_cmd, 'category2',
            'exploration.exp1.thread_12', None)
        score_categories = (
            suggestion_models.GeneralSuggestionModel.get_all_score_categories())
        self.assertIn(self.score_category, score_categories)
        self.assertIn('category1', score_categories)
        self.assertIn('category2', score_categories)

    def test_get_question_suggestions_waiting_longest_for_review(self) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category1',
            'skill1.thread1', self.question_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category2',
            'skill2.thread1', self.question_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_3', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category2',
            'skill3.thread1', self.question_language_code)

        question_suggestion_models = (
            suggestion_models.GeneralSuggestionModel
            .get_question_suggestions_waiting_longest_for_review()
        )

        self.assertEqual(len(question_suggestion_models), 3)
        # Assert that the order of the returned suggestion models represents
        # the suggestions sorted in descending order, based on how long each
        # suggestion has been waiting for review.
        self.assertEqual(question_suggestion_models[0].id, 'skill1.thread1')
        self.assertEqual(question_suggestion_models[1].id, 'skill2.thread1')
        self.assertEqual(question_suggestion_models[2].id, 'skill3.thread1')

    def test_get_translation_suggestions_waiting_longest_for_review_per_lang(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread1', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp2.thread1', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp3', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp3.thread1', self.translation_language_code)
        # Create a translation suggestion that has a different language code.
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp4', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp4.thread1', 'fr')

        translation_suggestion_models = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_waiting_longest_for_review(
                self.translation_language_code
            )
        )

        self.assertEqual(len(translation_suggestion_models), 3)
        # Assert that the order of the returned suggestion models represents
        # the suggestions sorted in descending order, based on how long each
        # suggestion has been waiting for review.
        self.assertEqual(
            translation_suggestion_models[0].id, 'exploration.exp1.thread1')
        self.assertEqual(
            translation_suggestion_models[1].id, 'exploration.exp2.thread1')
        self.assertEqual(
            translation_suggestion_models[2].id, 'exploration.exp3.thread1')

        translation_suggestion_models_with_different_lang_code = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_waiting_longest_for_review(
                'fr'
            )
        )

        self.assertEqual(len(
            translation_suggestion_models_with_different_lang_code), 1)
        self.assertEqual(
            translation_suggestion_models_with_different_lang_code[0].id,
            'exploration.exp4.thread1')

    def test_get_translation_suggestions_waiting_longest_for_review_wrong_lang(
        self
    ) -> None:
        translation_suggestion_models = (
            suggestion_models.GeneralSuggestionModel
            .get_translation_suggestions_waiting_longest_for_review(
                'wrong_language_code'
            )
        )

        self.assertEqual(len(translation_suggestion_models), 0)

    def test_get_translation_suggestions_waiting_longest_for_review_max_fetch(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp1.thread1', self.translation_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
            feconf.ENTITY_TYPE_EXPLORATION,
            'exp2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, self.score_category,
            'exploration.exp2.thread1', self.translation_language_code)

        with self.swap(
            suggestion_models,
            'MAX_TRANSLATION_SUGGESTIONS_TO_FETCH_FOR_REVIEWER_EMAILS', 1):
            translation_suggestion_models = (
                suggestion_models.GeneralSuggestionModel.
                get_translation_suggestions_waiting_longest_for_review(
                    self.translation_language_code)
            )

        # There should only be one translation suggestion returned since we
        # changed the maximum translations to fetch to 1.
        self.assertEqual(len(translation_suggestion_models), 1)
        self.assertEqual(
            translation_suggestion_models[0].id, 'exploration.exp1.thread1')

    def test_get_question_suggestions_waiting_longest_for_review_max_fetch(
        self
    ) -> None:
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_1', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category1',
            'skill1.thread1', self.question_language_code)
        suggestion_models.GeneralSuggestionModel.create(
            feconf.SUGGESTION_TYPE_ADD_QUESTION,
            feconf.ENTITY_TYPE_SKILL,
            'skill_2', self.target_version_at_submission,
            suggestion_models.STATUS_IN_REVIEW, 'author_3',
            'reviewer_2', self.change_cmd, 'category2',
            'skill2.thread1', self.question_language_code)

        with self.swap(
            suggestion_models,
            'MAX_QUESTION_SUGGESTIONS_TO_FETCH_FOR_REVIEWER_EMAILS', 1):
            question_suggestion_models = (
                suggestion_models.GeneralSuggestionModel
                .get_question_suggestions_waiting_longest_for_review()
            )

        # There should only be one question suggestion returned since we
        # changed the maximum questions to fetch to 1.
        self.assertEqual(len(question_suggestion_models), 1)
        self.assertEqual(question_suggestion_models[0].id, 'skill1.thread1')

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.GeneralSuggestionModel
            .export_data('non_existent_user'))
        test_data: Dict[str, str] = {}
        self.assertEqual(user_data, test_data)

    def test_export_data_nontrivial(self) -> None:
        test_export_suggestion_type = (
            feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT)
        test_export_target_type = feconf.ENTITY_TYPE_EXPLORATION
        test_export_target_id = self.target_id
        test_export_target_version = self.target_version_at_submission
        test_export_status = suggestion_models.STATUS_IN_REVIEW
        test_export_author = 'test_export_author'
        test_export_reviewer = 'test_export_reveiwer'
        test_export_change_cmd = self.change_cmd
        test_export_score_category = 'category1'
        test_export_thread_id = 'exploration.exp1.thread_export'
        test_export_language_code = 'en'
        test_export_edited_by_reviewer = False

        suggestion_models.GeneralSuggestionModel.create(
            test_export_suggestion_type,
            test_export_target_type,
            test_export_target_id,
            test_export_target_version,
            test_export_status,
            test_export_author,
            test_export_reviewer,
            test_export_change_cmd,
            test_export_score_category,
            test_export_thread_id,
            test_export_language_code
        )

        user_data = (
            suggestion_models.GeneralSuggestionModel
            .export_data('test_export_author'))

        test_data = {
            test_export_thread_id: {
                'suggestion_type': test_export_suggestion_type,
                'target_type': test_export_target_type,
                'target_id': test_export_target_id,
                'target_version_at_submission': test_export_target_version,
                'status': test_export_status,
                'change_cmd': test_export_change_cmd,
                'language_code': test_export_language_code,
                'edited_by_reviewer': test_export_edited_by_reviewer
            },

        }

        self.assertEqual(user_data, test_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'suggestion_type': base_models.EXPORT_POLICY.EXPORTED,
            'target_type': base_models.EXPORT_POLICY.EXPORTED,
            'target_id': base_models.EXPORT_POLICY.EXPORTED,
            'target_version_at_submission':
                base_models.EXPORT_POLICY.EXPORTED,
            'status': base_models.EXPORT_POLICY.EXPORTED,
            'author_id': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'final_reviewer_id': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'change_cmd': base_models.EXPORT_POLICY.EXPORTED,
            'score_category': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'language_code': base_models.EXPORT_POLICY.EXPORTED,
            'edited_by_reviewer': base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.GeneralSuggestionModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.GeneralSuggestionModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)


class CommunityContributionStatsModelUnitTests(test_utils.GenericTestBase):
    """Tests the CommunityContributionStatsModel 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

    def test_get_returns_community_contribution_stats_model_when_it_exists(
        self
    ) -> None:
        suggestion_models.CommunityContributionStatsModel(
            id=suggestion_models.COMMUNITY_CONTRIBUTION_STATS_MODEL_ID,
            translation_reviewer_counts_by_lang_code=(
                self.translation_reviewer_counts_by_lang_code),
            translation_suggestion_counts_by_lang_code=(
                self.translation_suggestion_counts_by_lang_code),
            question_reviewer_count=self.question_reviewer_count,
            question_suggestion_count=self.question_suggestion_count
        ).put()

        community_contribution_stats_model = (
            suggestion_models.CommunityContributionStatsModel.get()
        )

        self.assertEqual(
            community_contribution_stats_model.id,
            suggestion_models.COMMUNITY_CONTRIBUTION_STATS_MODEL_ID
        )
        self.assertEqual(
            (
                community_contribution_stats_model
                .translation_reviewer_counts_by_lang_code
            ),
            self.translation_reviewer_counts_by_lang_code
        )
        self.assertEqual(
            (
                community_contribution_stats_model
                .translation_suggestion_counts_by_lang_code
            ),
            self.translation_suggestion_counts_by_lang_code
        )
        self.assertEqual(
            community_contribution_stats_model.question_reviewer_count,
            self.question_reviewer_count
        )
        self.assertEqual(
            community_contribution_stats_model.question_suggestion_count,
            self.question_suggestion_count
        )

    def test_get_returns_new_community_contribution_stats_model_if_not_found(
        self
    ) -> None:
        """If the model has not been created yet, get should create the model
        with default values.
        """
        community_contribution_stats_model = (
            suggestion_models.CommunityContributionStatsModel.get()
        )
        self.assertEqual(
            community_contribution_stats_model.id,
            suggestion_models.COMMUNITY_CONTRIBUTION_STATS_MODEL_ID
        )
        self.assertEqual(
            (
                community_contribution_stats_model
                .translation_reviewer_counts_by_lang_code
            ), {}
        )
        self.assertEqual(
            (
                community_contribution_stats_model
                .translation_suggestion_counts_by_lang_code
            ), {}
        )
        self.assertEqual(
            community_contribution_stats_model.question_reviewer_count, 0
        )
        self.assertEqual(
            community_contribution_stats_model.question_suggestion_count, 0
        )

    def test_get_deletion_policy_returns_not_applicable(self) -> None:
        self.assertEqual(
            (
                suggestion_models.CommunityContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.NOT_APPLICABLE
        )

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'translation_reviewer_counts_by_lang_code':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'translation_suggestion_counts_by_lang_code':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'question_reviewer_count':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'question_suggestion_count':
                base_models.EXPORT_POLICY.NOT_APPLICABLE
        }
        model = suggestion_models.CommunityContributionStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.CommunityContributionStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.NOT_CORRESPONDING_TO_USER)


class TranslationContributionStatsModelUnitTests(test_utils.GenericTestBase):
    """Tests the TranslationContributionStatsModel class."""

    LANGUAGE_CODE: Final = 'es'
    CONTRIBUTOR_USER_ID: Final = 'uid_01234567890123456789012345678912'
    TOPIC_ID: Final = 'topic_id'
    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
    # Timestamp dates in sec since epoch for Mar 19 2021 UTC.
    CONTRIBUTION_DATES: Final = [
        datetime.date.fromtimestamp(1616173836),
        datetime.date.fromtimestamp(1616173837)
    ]

    def test_get_all_model_instances_matching_the_given_user_id(self) -> None:
        model = suggestion_models.TranslationContributionStatsModel
        self.assertEqual(
            model.get_all_by_user_id(self.CONTRIBUTOR_USER_ID), [])

        model.create(
            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
        )
        translation_contribution_stats_model = (
            model.get(
                self.LANGUAGE_CODE, self.CONTRIBUTOR_USER_ID, self.TOPIC_ID
            )
        )
        self.assertEqual(
            model.get_all_by_user_id(self.CONTRIBUTOR_USER_ID),
            [translation_contribution_stats_model]
        )

    def test_get_returns_model_when_it_exists(self) -> None:
        suggestion_models.TranslationContributionStatsModel.create(
            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
        )

        translation_contribution_stats_model = (
            suggestion_models.TranslationContributionStatsModel.get(
                self.LANGUAGE_CODE, self.CONTRIBUTOR_USER_ID, self.TOPIC_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert translation_contribution_stats_model is not None
        self.assertEqual(
            translation_contribution_stats_model.language_code,
            self.LANGUAGE_CODE
        )
        self.assertEqual(
            translation_contribution_stats_model.contributor_user_id,
            self.CONTRIBUTOR_USER_ID
        )
        self.assertEqual(
            translation_contribution_stats_model.submitted_translations_count,
            self.SUBMITTED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_contribution_stats_model
                .submitted_translation_word_count
            ),
            self.SUBMITTED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_contribution_stats_model.accepted_translations_count,
            self.ACCEPTED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_contribution_stats_model
                .accepted_translations_without_reviewer_edits_count
            ),
            self.ACCEPTED_TRANSLATIONS_WITHOUT_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            (
                translation_contribution_stats_model
                .accepted_translation_word_count
            ),
            self.ACCEPTED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_contribution_stats_model.rejected_translations_count,
            self.REJECTED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_contribution_stats_model
                .rejected_translation_word_count
            ),
            self.REJECTED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_contribution_stats_model.contribution_dates,
            self.CONTRIBUTION_DATES
        )

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.TranslationContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'language_code': base_models.EXPORT_POLICY.EXPORTED,
            'contributor_user_id':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_id': base_models.EXPORT_POLICY.EXPORTED,
            'submitted_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'submitted_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_without_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'contribution_dates': base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.TranslationContributionStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.TranslationContributionStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.TranslationContributionStatsModel.create(
            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
        )
        self.assertTrue(
            suggestion_models.TranslationContributionStatsModel
            .has_reference_to_user_id(self.CONTRIBUTOR_USER_ID))

        (
            suggestion_models.TranslationContributionStatsModel
            .apply_deletion_policy(self.CONTRIBUTOR_USER_ID)
        )

        self.assertFalse(
            suggestion_models.TranslationContributionStatsModel
            .has_reference_to_user_id(self.CONTRIBUTOR_USER_ID))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.TranslationContributionStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        topic_id_2 = 'topic ID 2'
        # Seed translation stats data for two different topics.
        model_1_id = suggestion_models.TranslationContributionStatsModel.create(
            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
        )
        model_2_id = suggestion_models.TranslationContributionStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            contributor_user_id=self.CONTRIBUTOR_USER_ID,
            topic_id=topic_id_2,
            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
        )
        dates_in_iso_format = [
            date.isoformat() for date in self.CONTRIBUTION_DATES]
        model_1_id_without_user_id = model_1_id.replace(
            '.%s.' % self.CONTRIBUTOR_USER_ID, '.'
        )
        model_2_id_without_user_id = model_2_id.replace(
            '.%s.' % self.CONTRIBUTOR_USER_ID, '.'
        )
        expected_data = {
            model_1_id_without_user_id: {
                'language_code': self.LANGUAGE_CODE,
                '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': dates_in_iso_format
            },
            model_2_id_without_user_id: {
                'language_code': self.LANGUAGE_CODE,
                'topic_id': topic_id_2,
                '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': dates_in_iso_format
            }
        }

        user_data = (
            suggestion_models.TranslationContributionStatsModel
            .export_data(self.CONTRIBUTOR_USER_ID))

        self.assertEqual(expected_data, user_data)


class TranslationReviewStatsModelUnitTests(test_utils.GenericTestBase):
    """Tests the TranslationContributionStatsModel class."""

    LANGUAGE_CODE = 'es'
    REVIEWER_USER_ID = 'uid_01234567890123456789012345678912'
    TOPIC_ID = 'topic_id'
    REVIEWED_TRANSLATIONS_COUNT = 2
    REVIEWED_TRANSLATION_WORD_COUNT = 100
    ACCEPTED_TRANSLATIONS_COUNT = 1
    ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT = 0
    ACCEPTED_TRANSLATION_WORD_COUNT = 50
    FIRST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_get_returns_model_when_it_exists(self) -> None:
        suggestion_models.TranslationReviewStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            reviewer_user_id=self.REVIEWER_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_translations_with_reviewer_edits_count=(
                self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
            accepted_translation_word_count=(
                self.ACCEPTED_TRANSLATION_WORD_COUNT),
            first_contribution_date=self.FIRST_CONTRIBUTION_DATE,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )

        translation_review_stats_model = (
            suggestion_models.TranslationReviewStatsModel.get(
                self.LANGUAGE_CODE, self.REVIEWER_USER_ID, self.TOPIC_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert translation_review_stats_model is not None
        self.assertEqual(
            translation_review_stats_model.language_code,
            self.LANGUAGE_CODE
        )
        self.assertEqual(
            translation_review_stats_model.reviewer_user_id,
            self.REVIEWER_USER_ID
        )
        self.assertEqual(
            translation_review_stats_model.reviewed_translations_count,
            self.REVIEWED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_review_stats_model
                .reviewed_translation_word_count
            ),
            self.REVIEWED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_review_stats_model.accepted_translations_count,
            self.ACCEPTED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_review_stats_model
                .accepted_translations_with_reviewer_edits_count
            ),
            self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            (
                translation_review_stats_model
                .accepted_translation_word_count
            ),
            self.ACCEPTED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_review_stats_model.first_contribution_date,
            self.FIRST_CONTRIBUTION_DATE
        )
        self.assertEqual(
            translation_review_stats_model.last_contribution_date,
            self.LAST_CONTRIBUTION_DATE
        )

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.TranslationReviewStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_get_all_by_user_id(self) -> None:
        suggestion_models.TranslationReviewStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            reviewer_user_id=self.REVIEWER_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_translations_with_reviewer_edits_count=(
                self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
            accepted_translation_word_count=(
                self.ACCEPTED_TRANSLATION_WORD_COUNT),
            first_contribution_date=self.FIRST_CONTRIBUTION_DATE,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )

        translation_review_stats_models = (
            suggestion_models.TranslationReviewStatsModel.get_all_by_user_id(
                self.REVIEWER_USER_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert translation_review_stats_models is not None

        self.assertEqual(
            len(translation_review_stats_models),
            1
        )

        translation_review_stats_model = translation_review_stats_models[0]

        self.assertEqual(
            translation_review_stats_model.language_code,
            self.LANGUAGE_CODE
        )
        self.assertEqual(
            translation_review_stats_model.reviewer_user_id,
            self.REVIEWER_USER_ID
        )
        self.assertEqual(
            translation_review_stats_model.reviewed_translations_count,
            self.REVIEWED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_review_stats_model
                .reviewed_translation_word_count
            ),
            self.REVIEWED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_review_stats_model.accepted_translations_count,
            self.ACCEPTED_TRANSLATIONS_COUNT
        )
        self.assertEqual(
            (
                translation_review_stats_model
                .accepted_translations_with_reviewer_edits_count
            ),
            self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            (
                translation_review_stats_model
                .accepted_translation_word_count
            ),
            self.ACCEPTED_TRANSLATION_WORD_COUNT
        )
        self.assertEqual(
            translation_review_stats_model.first_contribution_date,
            self.FIRST_CONTRIBUTION_DATE
        )
        self.assertEqual(
            translation_review_stats_model.last_contribution_date,
            self.LAST_CONTRIBUTION_DATE
        )

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.TranslationReviewStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            reviewer_user_id=self.REVIEWER_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_translations_with_reviewer_edits_count=(
                self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
            accepted_translation_word_count=(
                self.ACCEPTED_TRANSLATION_WORD_COUNT),
            first_contribution_date=self.FIRST_CONTRIBUTION_DATE,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        self.assertTrue(
            suggestion_models.TranslationReviewStatsModel
            .has_reference_to_user_id(self.REVIEWER_USER_ID))

        (
            suggestion_models.TranslationReviewStatsModel
            .apply_deletion_policy(self.REVIEWER_USER_ID)
        )

        self.assertFalse(
            suggestion_models.TranslationReviewStatsModel
            .has_reference_to_user_id(self.REVIEWER_USER_ID))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.TranslationReviewStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        topic_id_2 = 'topic ID 2'
        # Seed translation stats data for two different topics.
        model_1_id = suggestion_models.TranslationReviewStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            reviewer_user_id=self.REVIEWER_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_translations_with_reviewer_edits_count=(
                self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
            accepted_translation_word_count=(
                self.ACCEPTED_TRANSLATION_WORD_COUNT),
            first_contribution_date=self.FIRST_CONTRIBUTION_DATE,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        model_2_id = suggestion_models.TranslationReviewStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            reviewer_user_id=self.REVIEWER_USER_ID,
            topic_id=topic_id_2,
            reviewed_translations_count=self.REVIEWED_TRANSLATIONS_COUNT,
            reviewed_translation_word_count=(
                self.REVIEWED_TRANSLATION_WORD_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),
            first_contribution_date=self.FIRST_CONTRIBUTION_DATE,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        model_1_id_without_user_id = model_1_id.replace(
            '.%s.' % self.REVIEWER_USER_ID, '.'
        )
        model_2_id_without_user_id = model_2_id.replace(
            '.%s.' % self.REVIEWER_USER_ID, '.'
        )
        expected_data = {
            model_1_id_without_user_id: {
                'language_code': self.LANGUAGE_CODE,
                '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_translations_with_reviewer_edits_count': (
                    self.ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT),
                'accepted_translation_word_count': (
                    self.ACCEPTED_TRANSLATION_WORD_COUNT),
                'first_contribution_date': (
                    self.FIRST_CONTRIBUTION_DATE.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            },
            model_2_id_without_user_id: {
                'language_code': self.LANGUAGE_CODE,
                'topic_id': topic_id_2,
                'reviewed_translations_count': (
                    self.REVIEWED_TRANSLATIONS_COUNT),
                'reviewed_translation_word_count': (
                    self.REVIEWED_TRANSLATION_WORD_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),
                'first_contribution_date': (
                    self.FIRST_CONTRIBUTION_DATE.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            }
        }

        user_data = (
            suggestion_models.TranslationReviewStatsModel
            .export_data(self.REVIEWER_USER_ID))

        self.assertEqual(expected_data, user_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'language_code': base_models.EXPORT_POLICY.EXPORTED,
            'reviewer_user_id': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_id': base_models.EXPORT_POLICY.EXPORTED,
            'reviewed_translations_count': base_models.EXPORT_POLICY.EXPORTED,
            'reviewed_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_count': base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_with_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date': base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date': base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.TranslationReviewStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.TranslationReviewStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)


class QuestionContributionStatsModelUnitTests(test_utils.GenericTestBase):
    """Tests the QuestionContributionStatsModel class."""

    CONTRIBUTOR_USER_ID = 'uid_01234567890123456789012345678912'
    TOPIC_ID = 'topic_id'
    SUBMITTED_QUESTION_COUNT = 2
    ACCEPTED_QUESTIONS_COUNT = 1
    ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT = 0
    FIRST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_get_returns_model_when_it_exists(self) -> None:
        suggestion_models.QuestionContributionStatsModel.create(
            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
        )

        question_contribution_stats_model = (
            suggestion_models.QuestionContributionStatsModel.get(
                self.CONTRIBUTOR_USER_ID, self.TOPIC_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert question_contribution_stats_model is not None
        self.assertEqual(
            question_contribution_stats_model.contributor_user_id,
            self.CONTRIBUTOR_USER_ID
        )
        self.assertEqual(
            question_contribution_stats_model.submitted_questions_count,
            self.SUBMITTED_QUESTION_COUNT
        )
        self.assertEqual(
            question_contribution_stats_model.accepted_questions_count,
            self.ACCEPTED_QUESTIONS_COUNT
        )
        self.assertEqual(
            (
                question_contribution_stats_model
                .accepted_questions_without_reviewer_edits_count
            ),
            self.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            question_contribution_stats_model.first_contribution_date,
            self.FIRST_CONTRIBUTION_DATE
        )
        self.assertEqual(
            question_contribution_stats_model.last_contribution_date,
            self.LAST_CONTRIBUTION_DATE
        )

    def test_get_all_by_user_id(self) -> None:
        suggestion_models.QuestionContributionStatsModel.create(
            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
        )

        question_contribution_stats_models = (
            suggestion_models.QuestionContributionStatsModel.get_all_by_user_id(
                self.CONTRIBUTOR_USER_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert question_contribution_stats_models is not None

        self.assertEqual(
            len(question_contribution_stats_models),
            1
        )

        question_contribution_stats_model = question_contribution_stats_models[
            0]

        # Ruling out the possibility of None for mypy type checking.
        assert question_contribution_stats_model is not None
        self.assertEqual(
            question_contribution_stats_model.contributor_user_id,
            self.CONTRIBUTOR_USER_ID
        )
        self.assertEqual(
            question_contribution_stats_model.submitted_questions_count,
            self.SUBMITTED_QUESTION_COUNT
        )
        self.assertEqual(
            question_contribution_stats_model.accepted_questions_count,
            self.ACCEPTED_QUESTIONS_COUNT
        )
        self.assertEqual(
            (
                question_contribution_stats_model
                .accepted_questions_without_reviewer_edits_count
            ),
            self.ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            question_contribution_stats_model.first_contribution_date,
            self.FIRST_CONTRIBUTION_DATE
        )
        self.assertEqual(
            question_contribution_stats_model.last_contribution_date,
            self.LAST_CONTRIBUTION_DATE
        )

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.QuestionContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.QuestionContributionStatsModel.create(
            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
        )
        self.assertTrue(
            suggestion_models.QuestionContributionStatsModel
            .has_reference_to_user_id(self.CONTRIBUTOR_USER_ID))

        (
            suggestion_models.QuestionContributionStatsModel
            .apply_deletion_policy(self.CONTRIBUTOR_USER_ID)
        )

        self.assertFalse(
            suggestion_models.QuestionContributionStatsModel
            .has_reference_to_user_id(self.CONTRIBUTOR_USER_ID))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.QuestionContributionStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        topic_id_2 = 'topic ID 2'
        # Seed question stats data for two different topics.
        suggestion_models.QuestionContributionStatsModel.create(
            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
        )
        suggestion_models.QuestionContributionStatsModel.create(
            contributor_user_id=self.CONTRIBUTOR_USER_ID,
            topic_id=topic_id_2,
            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
        )
        model_1_id_without_user_id = self.TOPIC_ID
        model_2_id_without_user_id = topic_id_2
        expected_data = {
            model_1_id_without_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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            },
            model_2_id_without_user_id: {
                'topic_id': topic_id_2,
                '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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            }
        }

        user_data = (
            suggestion_models.QuestionContributionStatsModel
            .export_data(self.CONTRIBUTOR_USER_ID))

        self.assertEqual(expected_data, user_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'contributor_user_id': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_id': base_models.EXPORT_POLICY.EXPORTED,
            'submitted_questions_count': base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_count': base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_without_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date': base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date': base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.QuestionContributionStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.QuestionContributionStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)


class QuestionReviewStatsModelUnitTests(test_utils.GenericTestBase):
    """Tests the QuestionReviewStatsModel class."""

    REVIEWER_USER_ID = 'uid_01234567890123456789012345678912'
    TOPIC_ID = 'topic_id'
    REVIEWED_QUESTIONS_COUNT = 2
    ACCEPTED_QUESTIONS_COUNT = 1
    ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT = 0
    FIRST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_get_returns_model_when_it_exists(self) -> None:
        suggestion_models.QuestionReviewStatsModel.create(
            reviewer_user_id=self.REVIEWER_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
        )

        question_review_stats_model = (
            suggestion_models.QuestionReviewStatsModel.get(
                self.REVIEWER_USER_ID, self.TOPIC_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert question_review_stats_model is not None
        self.assertEqual(
            question_review_stats_model.reviewer_user_id,
            self.REVIEWER_USER_ID
        )
        self.assertEqual(
            question_review_stats_model.reviewed_questions_count,
            self.REVIEWED_QUESTIONS_COUNT
        )
        self.assertEqual(
            question_review_stats_model.accepted_questions_count,
            self.ACCEPTED_QUESTIONS_COUNT
        )
        self.assertEqual(
            (
                question_review_stats_model
                .accepted_questions_with_reviewer_edits_count
            ),
            self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            question_review_stats_model.first_contribution_date,
            self.FIRST_CONTRIBUTION_DATE
        )
        self.assertEqual(
            question_review_stats_model.last_contribution_date,
            self.LAST_CONTRIBUTION_DATE
        )

    def test_get_all_by_user_id(self) -> None:
        suggestion_models.QuestionReviewStatsModel.create(
            reviewer_user_id=self.REVIEWER_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
        )

        question_review_stats_models = (
            suggestion_models.QuestionReviewStatsModel.get_all_by_user_id(
                self.REVIEWER_USER_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert question_review_stats_models is not None

        question_review_stats_model = question_review_stats_models[0]

        self.assertEqual(
            question_review_stats_model.reviewer_user_id,
            self.REVIEWER_USER_ID
        )
        self.assertEqual(
            question_review_stats_model.reviewed_questions_count,
            self.REVIEWED_QUESTIONS_COUNT
        )
        self.assertEqual(
            question_review_stats_model.accepted_questions_count,
            self.ACCEPTED_QUESTIONS_COUNT
        )
        self.assertEqual(
            (
                question_review_stats_model
                .accepted_questions_with_reviewer_edits_count
            ),
            self.ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT
        )
        self.assertEqual(
            question_review_stats_model.first_contribution_date,
            self.FIRST_CONTRIBUTION_DATE
        )
        self.assertEqual(
            question_review_stats_model.last_contribution_date,
            self.LAST_CONTRIBUTION_DATE
        )

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.QuestionReviewStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.QuestionReviewStatsModel.create(
            reviewer_user_id=self.REVIEWER_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
        )
        self.assertTrue(
            suggestion_models.QuestionReviewStatsModel
            .has_reference_to_user_id(self.REVIEWER_USER_ID))

        (
            suggestion_models.QuestionReviewStatsModel
            .apply_deletion_policy(self.REVIEWER_USER_ID)
        )

        self.assertFalse(
            suggestion_models.QuestionReviewStatsModel
            .has_reference_to_user_id(self.REVIEWER_USER_ID))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.QuestionReviewStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        topic_id_2 = 'topic ID 2'
        # Seed question stats data for two different topics.
        suggestion_models.QuestionReviewStatsModel.create(
            reviewer_user_id=self.REVIEWER_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
        )
        suggestion_models.QuestionReviewStatsModel.create(
            reviewer_user_id=self.REVIEWER_USER_ID,
            topic_id=topic_id_2,
            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
        )
        model_1_id_without_user_id = self.TOPIC_ID
        model_2_id_without_user_id = topic_id_2
        expected_data = {
            model_1_id_without_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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            },
            model_2_id_without_user_id: {
                'topic_id': topic_id_2,
                '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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            }
        }

        user_data = (
            suggestion_models.QuestionReviewStatsModel
            .export_data(self.REVIEWER_USER_ID))

        self.assertEqual(expected_data, user_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'reviewer_user_id': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_id': base_models.EXPORT_POLICY.EXPORTED,
            'reviewed_questions_count': base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_count': base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_with_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date': base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date': base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.QuestionReviewStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.QuestionReviewStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)


class TranslationSubmitterTotalContributionStatsModelUnitTests(
    test_utils.GenericTestBase):
    """Tests the TranslationSubmitterTotalContributionStatsModel class."""

    SUGGESTION_LANGUAGE_CODE: Final = 'es'
    USER_ID_1: Final = 'uid_01234567890123456789012345678912'
    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.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_get_all_model_instances_matching_the_given_user_id(self) -> None:
        model = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel)
        self.assertEqual(
            model.get_all_by_user_id(self.USER_ID_1), [])

        model.create(
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        translation_submitter_total_contribution_stats = (
            model.get(
                self.SUGGESTION_LANGUAGE_CODE, self.USER_ID_1
            )
        )
        self.assertEqual(
            model.get_all_by_user_id(self.USER_ID_1),
            [translation_submitter_total_contribution_stats]
        )

    def test_fetch_page_with_sorting(self) -> None:
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_1',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=1,
            overall_accuracy=4,
            submitted_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_2',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=2,
            overall_accuracy=3,
            submitted_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_3',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1', 'topic2'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=3,
            overall_accuracy=2,
            submitted_translations_count=50,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_4',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=4,
            overall_accuracy=1,
            submitted_translations_count=4,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()

        # Check for decreasing performance(default) sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 3)

        # Check for non-performance sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_PERFORMANCE.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for decreasing Accuracy sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_ACCURACY.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for increasing Accuracy sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_ACCURACY.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for decreasing Translated cards sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_SUBMISSIONS.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for increasing Translated cards sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_SUBMISSIONS.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for decreasing last activity sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_LAST_ACTIVITY.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for increasing last activity sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_LAST_ACTIVITY.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

    def test_fetch_page_with_filtering(self) -> None:
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_1',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=1,
            overall_accuracy=4,
            submitted_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_2',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=2,
            overall_accuracy=2,
            submitted_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_3',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1', 'topic2'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=3,
            overall_accuracy=1,
            submitted_translations_count=50,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_4',
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=4,
            overall_accuracy=1,
            submitted_translations_count=4,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()

        # Check for 'es' language filter.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for 'hi' language filter.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='hi'
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertFalse(more)
        self.assertEqual(next_offset, 1)

        # Check for topic filter ['topic1', 'topic2'].
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                topic_ids=['topic1', 'topic2'],
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 2)

        # Check for last activity under 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=7,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for last activity under 90 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=90,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for no sorted_results in given time.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=7,
                language_code='hi'
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 1)

    def test_fetch_page_with_sorting_and_filtering(self) -> None:
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_1',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=1,
            overall_accuracy=4,
            submitted_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_2',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=2,
            overall_accuracy=2,
            submitted_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_3',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1', 'topic2'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=3,
            overall_accuracy=1,
            submitted_translations_count=50,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_4',
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=4,
            overall_accuracy=1,
            submitted_translations_count=4,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()

        # Check for topic filter and non-performance sort order.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_PERFORMANCE.value,
                topic_ids=['topic1', 'topic2'],
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 2)

        # Check for decreasing last activity sort order and
        # topic filter.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_LAST_ACTIVITY.value,
                topic_ids=['topic1', 'topic2'],
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 2)

    def test_fetch_page_with_pagination(self) -> None:
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_1',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=1,
            overall_accuracy=4,
            submitted_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
            datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_2',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=2,
            overall_accuracy=3,
            submitted_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_3',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic1', 'topic2'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=3,
            overall_accuracy=2,
            submitted_translations_count=50,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.TranslationSubmitterTotalContributionStatsModel(
            id='model_4',
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=4,
            overall_accuracy=1,
            submitted_translations_count=4,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()

        # Check for pagination with offset 2.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=2,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for pagination with no results.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=4,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_PERFORMANCE.value,
                topic_ids=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

    def test_has_reference_to_user_id(self) -> None:
        model = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel)
        model.create(
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        self.assertTrue(
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1)
        )
        self.assertFalse(
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .has_reference_to_user_id('non-existent_user')
        )

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models
                .TranslationSubmitterTotalContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'language_code':
                base_models.EXPORT_POLICY.EXPORTED,
            # User ID is not exported in order to keep internal ids private.
            'contributor_id':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_ids_with_translation_submissions':
                base_models.EXPORT_POLICY.EXPORTED,
            'recent_review_outcomes':
                base_models.EXPORT_POLICY.EXPORTED,
            'recent_performance':
                base_models.EXPORT_POLICY.EXPORTED,
            'overall_accuracy':
                base_models.EXPORT_POLICY.EXPORTED,
            'submitted_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'submitted_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_without_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED
        }
        model = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel)
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel)
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.TranslationSubmitterTotalContributionStatsModel.create( # pylint: disable=line-too-long
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        self.assertTrue(
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

        (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .apply_deletion_policy(self.USER_ID_1)
        )

        self.assertFalse(
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        # Seed translation stats data for two different languages.
        suggestion_models.TranslationSubmitterTotalContributionStatsModel.create( # pylint: disable=line-too-long
            language_code=self.SUGGESTION_LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=(
                self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        suggestion_models.TranslationSubmitterTotalContributionStatsModel.create( # pylint: disable=line-too-long
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_submissions=[
                'topic3', 'topic4'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        model_1_id_without_user_id = self.SUGGESTION_LANGUAGE_CODE
        model_2_id_without_user_id = 'hi'
        expected_data = {
            model_1_id_without_user_id: {
                'language_code': self.SUGGESTION_LANGUAGE_CODE,
                'topic_ids_with_translation_submissions': (
                    self.TOPIC_IDS_WITH_TRANSLATION_SUBMISSIONS),
                'recent_review_outcomes': self.RECENT_REVIEW_OUTCOMES,
                '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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            },
            model_2_id_without_user_id: {
                'language_code': 'hi',
                'topic_ids_with_translation_submissions': [
                    'topic3', 'topic4'
                ],
                'recent_review_outcomes': self.RECENT_REVIEW_OUTCOMES,
                '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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            }
        }

        user_data = (
            suggestion_models.TranslationSubmitterTotalContributionStatsModel
            .export_data(self.USER_ID_1))

        self.assertEqual(expected_data, user_data)


class TranslationReviewerTotalContributionStatsModelUnitTests(
        test_utils.GenericTestBase):
    """Tests the TranslationReviewerTotalContributionStatsModel class."""

    LANGUAGE_CODE = 'es'
    USER_ID_1 = 'uid_01234567890123456789012345678912'
    TOPIC_IDS_WITH_TRANSLATION_REVIEWS = ['18', '19', '20']
    REVIEWED_TRANSLATIONS_COUNT = 2
    ACCEPTED_TRANSLATIONS_COUNT = 1
    ACCEPTED_TRANSLATIONS_WITH_REVIEWER_EDITS_COUNT = 0
    ACCEPTED_TRANSLATION_WORD_COUNT = 50
    REJECTED_TRANSLATIONS_COUNT = 0
    FIRST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.TranslationReviewerTotalContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_get_all_by_user_id(self) -> None:
        model = suggestion_models.TranslationReviewerTotalContributionStatsModel
        self.assertEqual(
            model.get_all_by_user_id(self.USER_ID_1), [])

        model.create(
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        translation_reviewer_total_contribution_stats = (
            model.get(
                self.LANGUAGE_CODE, self.USER_ID_1
            )
        )
        self.assertEqual(
            model.get_all_by_user_id(self.USER_ID_1),
            [translation_reviewer_total_contribution_stats]
        )

    def test_fetch_page_with_sorting(self) -> None:
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_1',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_2',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_3',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=30,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_4',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=40,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(155))
        ).put()

        # Check for decreasing order of reviewed cards(default)
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for increasing order of reviewed cards.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_TRANSLATIONS.value,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for decreasing order of last activity.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_LAST_ACTIVITY.value,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for increasing order of last activity.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_LAST_ACTIVITY.value,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 3)

    def test_fetch_page_with_filtering(self) -> None:
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_1',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_2',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_3',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=30,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_4',
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()

        # Check for 'es' language filter.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for 'hi' language filter.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=None,
                language_code='hi'
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertFalse(more)
        self.assertEqual(next_offset, 1)

        # Check for max_days_since_last_activity filter within 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=7,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for max_days_since_last_activity filter within 90 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=90,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for no sorted_results within 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=7,
                language_code='hi'
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 1)

    def test_fetch_page_with_sorting_and_filtering(self) -> None:
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_1',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.date.today()
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_2',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_3',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=30,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_4',
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()

        # Check for language filter and IncreasingLastActivity sort.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_LAST_ACTIVITY.value,
                max_days_since_last_activity=None,
                language_code='hi'
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertFalse(more)
        self.assertEqual(next_offset, 1)

        # Check for max_days_since_last_activity filter within 7 days
        # and IncreasingReviewedTranslations sort.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_TRANSLATIONS.value,
                max_days_since_last_activity=7,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

    def test_fetch_page_with_pagination(self) -> None:
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_1',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_2',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_3',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=30,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_4',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=40,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()

        # Check for Pagination with offset 2.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=2,
                sort_by=None,
                max_days_since_last_activity=None,
                language_code='es'
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

    def test_fetch_page_covering_all_branches(self) -> None:
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_1',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=10,
            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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_2',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=20,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_3',
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                self.TOPIC_IDS_WITH_TRANSLATION_REVIEWS),
            reviewed_translations_count=30,
            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=datetime.datetime.utcnow(),
            last_contribution_date=datetime.datetime.utcnow()
        ).put()
        suggestion_models.TranslationReviewerTotalContributionStatsModel(
            id='model_4',
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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=datetime.datetime.utcnow(),
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(65))
        ).put()

        # Check for last_acitvity filter within 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
            page_size=2,
            offset=0,
            sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_TRANSLATIONS.value,
            max_days_since_last_activity=7,
            language_code='es'
            ))
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for last_acitvity filter within 90 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
            page_size=1,
            offset=0,
            sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_TRANSLATIONS.value,
            max_days_since_last_activity=90,
            language_code='es'
            ))
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertTrue(more)
        self.assertEqual(next_offset, 1)

        # Check for no sorted_results within 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .fetch_page(
            page_size=1,
            offset=0,
            sort_by=None,
            max_days_since_last_activity=7,
            language_code='hi'
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 1)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.TranslationReviewerTotalContributionStatsModel.create(
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        self.assertTrue(
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

        (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .apply_deletion_policy(self.USER_ID_1)
        )

        self.assertFalse(
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        # Seed translation stats data for two different languages.
        suggestion_models.TranslationReviewerTotalContributionStatsModel.create( # pylint: disable=line-too-long
            language_code=self.LANGUAGE_CODE,
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        suggestion_models.TranslationReviewerTotalContributionStatsModel.create( # pylint: disable=line-too-long
            language_code='hi',
            contributor_id=self.USER_ID_1,
            topic_ids_with_translation_reviews=(
                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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        model_1_id_without_user_id = self.LANGUAGE_CODE
        model_2_id_without_user_id = 'hi'
        expected_data = {
            model_1_id_without_user_id: {
                'language_code': self.LANGUAGE_CODE,
                'topic_ids_with_translation_reviews': (
                    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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            },
            model_2_id_without_user_id: {
                'language_code': 'hi',
                'topic_ids_with_translation_reviews': (
                    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.isoformat()),
                'last_contribution_date': (
                    self.LAST_CONTRIBUTION_DATE.isoformat())
            }
        }

        user_data = (
            suggestion_models.TranslationReviewerTotalContributionStatsModel
            .export_data(self.USER_ID_1))

        self.assertEqual(expected_data, user_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'language_code':
                base_models.EXPORT_POLICY.EXPORTED,
            # User ID is not exported in order to keep internal ids private.
            'contributor_id':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_ids_with_translation_reviews':
                base_models.EXPORT_POLICY.EXPORTED,
            'reviewed_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translations_with_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_translation_word_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_translations_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.TranslationReviewerTotalContributionStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.TranslationReviewerTotalContributionStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER)


class QuestionSubmitterTotalContributionStatsModelUnitTests(
    test_utils.GenericTestBase):
    """Tests the QuestionSubmitterTotalContributionStatsModel class."""

    USER_ID_1 = 'uid_01234567890123456789012345678911'
    USER_ID_2 = 'uid_01234567890123456789012345678912'
    USER_ID_3 = 'uid_01234567890123456789012345678913'
    USER_ID_4 = 'uid_01234567890123456789012345678914'
    TOPIC_IDS_WITH_QUESTION_SUBMISSIONS = [
        'topic1', 'topic2', 'topic3'
    ]
    RECENT_REVIEW_OUTCOMES = ['accepted', 'rejected']
    RECENT_PERFORMANCE = 20
    OVERALL_ACCURACY = 2.0
    SUBMITTED_QUESTIONS_COUNT = 2
    ACCEPTED_QUESTIONS_COUNT = 1
    ACCEPTED_QUESTIONS_WITHOUT_REVIEWER_EDITS_COUNT = 0
    REJECTED_QUESTIONS_COUNT = 20
    FIRST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_fetch_page_with_sorting(self) -> None:
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=10,
            overall_accuracy=30.0,
            submitted_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_submissions=[
                'topic2', 'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=20,
            overall_accuracy=20.0,
            submitted_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=30,
            overall_accuracy=10.0,
            submitted_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_4',
            contributor_id=self.USER_ID_3,
            topic_ids_with_question_submissions=[
                'topic4'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=40,
            overall_accuracy=5.0,
            submitted_questions_count=40,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for decreasing performance(default) sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for increasing performance sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_PERFORMANCE.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for decreasing Accuracy sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_ACCURACY.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 3)

        # Check for increasing Accuracy sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_ACCURACY.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 4)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for decreasing Submitted cards sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_SUBMISSIONS.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 3)

        # Check for increasing Submitted cards sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_SUBMISSIONS.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 3)

        # Check for decreasing last activity sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_LAST_ACTIVITY.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for increasing last activity sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_LAST_ACTIVITY.value,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

    def test_fetch_page_with_filtering(self) -> None:
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=10,
            overall_accuracy=30.0,
            submitted_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_submissions=[
                'topic2', 'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=20,
            overall_accuracy=20.0,
            submitted_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=30,
            overall_accuracy=10.0,
            submitted_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_4',
            contributor_id=self.USER_ID_3,
            topic_ids_with_question_submissions=[
                'topic4'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=40,
            overall_accuracy=5.0,
            submitted_questions_count=40,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for topic filter.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=4,
                offset=0,
                sort_by=None,
                topic_ids=['topic1', 'topic2'],
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 2)

        # Check for max_days_since_last_activity under 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=7
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for max_days_since_last_activity under 90 days.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=90
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_4')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for no sorted_results in given time.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=0,
                sort_by=None,
                topic_ids=[
                    'non_existent_topic'
                ],
                max_days_since_last_activity=7
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 0)

    def test_fetch_page_with_sorting_and_filtering(self) -> None:
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=10,
            overall_accuracy=30.0,
            submitted_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_submissions=[
                'topic2', 'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=20,
            overall_accuracy=20.0,
            submitted_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=30,
            overall_accuracy=10.0,
            submitted_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_4',
            contributor_id=self.USER_ID_3,
            topic_ids_with_question_submissions=[
                'topic4'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=40,
            overall_accuracy=5.0,
            submitted_questions_count=40,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for topic filter and non-performance sort order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=1,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_PERFORMANCE.value,
                topic_ids=['topic1', 'topic2'],
                max_days_since_last_activity=None
            ))
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 2)

        # Check for max_days_since_last_activity in 90 days
        # and DecreasingLastActivity order.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_LAST_ACTIVITY.value,
                topic_ids=None,
                max_days_since_last_activity=90
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_4')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

    def test_fetch_page_with_pagination(self) -> None:
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic1'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=10,
            overall_accuracy=30.0,
            submitted_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(125))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_submissions=[
                'topic2', 'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=20,
            overall_accuracy=20.0,
            submitted_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=[
                'topic3'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=30,
            overall_accuracy=10.0,
            submitted_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionSubmitterTotalContributionStatsModel(
            id='model_4',
            contributor_id=self.USER_ID_3,
            topic_ids_with_question_submissions=[
                'topic4'
            ],
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            recent_performance=40,
            overall_accuracy=5.0,
            submitted_questions_count=40,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for pagination with offset=2.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=2,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

        # Check for pagination with no results.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=4,
                sort_by=None,
                topic_ids=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 4)

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.QuestionSubmitterTotalContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.QuestionSubmitterTotalContributionStatsModel.create(
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=(
                self.TOPIC_IDS_WITH_QUESTION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        self.assertTrue(
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

        (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .apply_deletion_policy(self.USER_ID_1)
        )

        self.assertFalse(
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        suggestion_models.QuestionSubmitterTotalContributionStatsModel.create(
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_submissions=(
                self.TOPIC_IDS_WITH_QUESTION_SUBMISSIONS),
            recent_review_outcomes=self.RECENT_REVIEW_OUTCOMES,
            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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        expected_data = {
            'topic_ids_with_question_submissions': (
                self.TOPIC_IDS_WITH_QUESTION_SUBMISSIONS),
            'recent_review_outcomes': self.RECENT_REVIEW_OUTCOMES,
            '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.isoformat()),
            'last_contribution_date': (
                self.LAST_CONTRIBUTION_DATE.isoformat())
        }

        user_data = (
            suggestion_models.QuestionSubmitterTotalContributionStatsModel
            .export_data(self.USER_ID_1))

        self.assertEqual(expected_data, user_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'contributor_id':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_ids_with_question_submissions':
                base_models.EXPORT_POLICY.EXPORTED,
            'recent_review_outcomes':
                base_models.EXPORT_POLICY.EXPORTED,
            'recent_performance':
                base_models.EXPORT_POLICY.EXPORTED,
            'overall_accuracy':
                base_models.EXPORT_POLICY.EXPORTED,
            'submitted_questions_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_without_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_questions_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.QuestionSubmitterTotalContributionStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.QuestionSubmitterTotalContributionStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.ONE_INSTANCE_PER_USER)


class QuestionReviewerTotalContributionStatsModelUnitTests(
    test_utils.GenericTestBase):
    """Tests the QuestionReviewerTotalContributionStatsModel class."""

    USER_ID_1 = 'uid_01234567890123456789012345678911'
    USER_ID_2 = 'uid_01234567890123456789012345678912'
    USER_ID_3 = 'uid_01234567890123456789012345678913'
    USER_ID_4 = 'uid_01234567890123456789012345678914'
    TOPIC_IDS_WITH_QUESTION_REVIEWS: Final = ['18', '19', '20']
    REVIEWED_QUESTIONS_COUNT = 2
    ACCEPTED_QUESTIONS_COUNT = 1
    ACCEPTED_QUESTIONS_WITH_REVIEWER_EDITS_COUNT = 0
    REJECTED_QUESTIONS_COUNT: Final = 20
    FIRST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)
    LAST_CONTRIBUTION_DATE = datetime.date.fromtimestamp(1616173836)

    def test_fetch_page_with_sorting(self) -> None:
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1'
            ],
            reviewed_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1', 'topic2'
            ],
            reviewed_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_reviews=[
                'topic3'
            ],
            reviewed_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for decreasing order of reviewed questions(default)
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for increasing order of reviewed questions.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=3,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_QUESTIONS.value,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 3)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for decreasing order of last activity.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_DECREASING_LAST_ACTIVITY.value,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_1')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for increasing order of last activity.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_LAST_ACTIVITY.value,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

    def test_fetch_page_with_filtering(self) -> None:
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1'
            ],
            reviewed_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1', 'topic2'
            ],
            reviewed_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_reviews=[
                'topic3'
            ],
            reviewed_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for max_days_since_last_activity filter within 7 days.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=7
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for max_days_since_last_activity filter within 90 days.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=90
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

        # Check for no sorted_results within 1 day.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=1,
                offset=0,
                sort_by=None,
                max_days_since_last_activity=1
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

    def test_fetch_page_with_sorting_and_filtering(self) -> None:
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1'
            ],
            reviewed_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1', 'topic2'
            ],
            reviewed_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_reviews=[
                'topic3'
            ],
            reviewed_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for max_days_since_last_activity filter within 90 days
        # and IncreasingReviewedQuestions sort.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_QUESTIONS.value,
                max_days_since_last_activity=90
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for max_days_since_last_activity filter within 7 days
        # and IncreasingReviewedQuestions sort.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_REVIEWED_QUESTIONS.value,
                max_days_since_last_activity=7
            ))
        self.assertEqual(len(sorted_results), 1)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for max_days_since_last_activity filter within 90 days
        # and IncreasingLastActivity sort.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=0,
                sort_by=suggestion_models.
                    SortChoices.SORT_KEY_INCREASING_LAST_ACTIVITY.value,
                max_days_since_last_activity=90
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_3')
        self.assertEqual(sorted_results[1].id, 'model_2')
        self.assertTrue(more)
        self.assertEqual(next_offset, 2)

    def test_fetch_page_with_pagination(self) -> None:
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_1',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1'
            ],
            reviewed_questions_count=10,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(95))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_2',
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=[
                'topic1', 'topic2'
            ],
            reviewed_questions_count=20,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(35))
        ).put()
        suggestion_models.QuestionReviewerTotalContributionStatsModel(
            id='model_3',
            contributor_id=self.USER_ID_2,
            topic_ids_with_question_reviews=[
                'topic3'
            ],
            reviewed_questions_count=30,
            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,
            last_contribution_date=(
                datetime.date.today() - datetime.timedelta(5))
        ).put()

        # Check for pagination with offset=1.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=1,
                sort_by=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 2)
        self.assertEqual(sorted_results[0].id, 'model_2')
        self.assertEqual(sorted_results[1].id, 'model_1')
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

        # Check for pagination with no results.
        sorted_results, next_offset, more = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .fetch_page(
                page_size=2,
                offset=3,
                sort_by=None,
                max_days_since_last_activity=None
            ))
        self.assertEqual(len(sorted_results), 0)
        self.assertFalse(more)
        self.assertEqual(next_offset, 3)

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            (
                suggestion_models.QuestionReviewerTotalContributionStatsModel
                .get_deletion_policy()
            ),
            base_models.DELETION_POLICY.DELETE)

    def test_apply_deletion_policy(self) -> None:
        suggestion_models.QuestionReviewerTotalContributionStatsModel.create(
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=(
                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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        self.assertTrue(
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

        (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .apply_deletion_policy(self.USER_ID_1)
        )

        self.assertFalse(
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .has_reference_to_user_id(self.USER_ID_1))

    def test_export_data_trivial(self) -> None:
        user_data = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .export_data('non_existent_user'))
        self.assertEqual(user_data, {})

    def test_export_data_nontrivial(self) -> None:
        suggestion_models.QuestionReviewerTotalContributionStatsModel.create(
            contributor_id=self.USER_ID_1,
            topic_ids_with_question_reviews=(
                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,
            last_contribution_date=self.LAST_CONTRIBUTION_DATE
        )
        expected_data = {
            'topic_ids_with_question_reviews': (
                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.isoformat()),
            'last_contribution_date': (
                self.LAST_CONTRIBUTION_DATE.isoformat())
        }

        user_data = (
            suggestion_models.QuestionReviewerTotalContributionStatsModel
            .export_data(self.USER_ID_1))

        self.assertEqual(expected_data, user_data)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'contributor_id':
                base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'topic_ids_with_question_reviews':
                base_models.EXPORT_POLICY.EXPORTED,
            'reviewed_questions_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'accepted_questions_with_reviewer_edits_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'rejected_questions_count':
                base_models.EXPORT_POLICY.EXPORTED,
            'first_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED,
            'last_contribution_date':
                base_models.EXPORT_POLICY.EXPORTED
        }
        model = suggestion_models.QuestionReviewerTotalContributionStatsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.QuestionReviewerTotalContributionStatsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER.ONE_INSTANCE_PER_USER)


class SortChoicesUnitTest(test_utils.GenericTestBase):
    """Test for checking enum SortChoices matches with values in
    CD_ADMIN_STATS_SORT_OPTIONS in constants.ts.
    """

    def test_enum_dictionary_match(self) -> None:
        enum_values = []

        for enum_member in suggestion_models.SortChoices:
            enum_values.append(enum_member.value)

        dict_values = list(
            constants.CD_ADMIN_STATS_SORT_OPTIONS.keys())

        enum_values.sort()
        dict_values.sort()

        self.assertEqual(enum_values, dict_values)


class TranslationCoordinatorsModelUnitTests(test_utils.GenericTestBase):
    """Tests the TranslationCoordinatorsModel class."""

    LANGUAGE_1_ID: Final = 'language_1_id'
    LANGUAGE_2_ID: Final = 'language_2_id'
    LANGUAGE_3_ID: Final = 'language_3_id'
    LANGUAGE_4_ID: Final = 'language_4_id'
    LANGUAGE_5_ID: Final = 'language_5_id'
    USER_ID_1: Final = 'user_id_1'
    USER_ID_2: Final = 'user_id_2'

    def setUp(self) -> None:
        super().setUp()
        suggestion_models.TranslationCoordinatorsModel(
            id=self.LANGUAGE_4_ID,
            coordinator_ids=[self.USER_ID_2],
            coordinators_count=1
        ).put()
        suggestion_models.TranslationCoordinatorsModel(
            id=self.LANGUAGE_5_ID,
            coordinator_ids=[self.USER_ID_2],
            coordinators_count=1
        ).put()

    def test_get_deletion_policy(self) -> None:
        self.assertEqual(
            suggestion_models.TranslationCoordinatorsModel
            .get_deletion_policy(),
            base_models.DELETION_POLICY.LOCALLY_PSEUDONYMIZE)

    def test_has_reference_to_user_id(self) -> None:
        language_coordinator_model = (
            suggestion_models.TranslationCoordinatorsModel(
            id=self.LANGUAGE_1_ID, coordinator_ids=['coordinator_id'],
            coordinators_count=1))
        language_coordinator_model.put()
        self.assertTrue(
            suggestion_models.TranslationCoordinatorsModel
            .has_reference_to_user_id('coordinator_id'))
        self.assertFalse(
            suggestion_models.TranslationCoordinatorsModel
            .has_reference_to_user_id('x_id'))

    def test_export_data_nontrivial(self) -> None:
        """Tests nontrivial export data on user with some coordinated
        languages.
        """
        user_data = suggestion_models.TranslationCoordinatorsModel.export_data(
            self.USER_ID_2)
        expected_data = {
            'coordinated_language_ids': [self.LANGUAGE_4_ID, self.LANGUAGE_5_ID]
        }
        self.assertEqual(user_data, expected_data)

    def test_export_data_trivial(self) -> None:
        """Tests trivial export data on user with no coordinated languages."""
        user_data = suggestion_models.TranslationCoordinatorsModel.export_data(
            self.USER_ID_1)
        expected_data: Dict[str, List[str]] = {
            'coordinated_language_ids': []
        }
        self.assertEqual(user_data, expected_data)

    def test_get_model_association_to_user(self) -> None:
        model = suggestion_models.TranslationCoordinatorsModel
        self.assertEqual(
            model.get_model_association_to_user(),
            base_models.MODEL_ASSOCIATION_TO_USER
            .ONE_INSTANCE_SHARED_ACROSS_USERS)

    def test_get_export_policy(self) -> None:
        expected_dict = {
            'created_on': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
            'coordinator_ids': base_models.EXPORT_POLICY.EXPORTED,
            'coordinators_count': base_models.EXPORT_POLICY.NOT_APPLICABLE
        }
        model = suggestion_models.TranslationCoordinatorsModel
        self.assertEqual(model.get_export_policy(), expected_dict)

    def test_get_field_name_mapping_to_takeout_keys(self) -> None:
        self.assertEqual(
            suggestion_models.TranslationCoordinatorsModel.
            get_field_name_mapping_to_takeout_keys(),
            {
                'coordinator_ids': 'coordinated_language_ids'
            })

    def test_get_returns_model_when_it_exists(self) -> None:
        translation_coordinators_model = (
            suggestion_models.TranslationCoordinatorsModel.get(
                self.LANGUAGE_4_ID
            )
        )

        # Ruling out the possibility of None for mypy type checking.
        assert translation_coordinators_model is not None
        self.assertEqual(
            translation_coordinators_model.id,
            self.LANGUAGE_4_ID
        )

    def test_get_model_by_user_id(self) -> None:
        translation_coordinators_models = (
            suggestion_models.TranslationCoordinatorsModel.get_by_user(
                self.USER_ID_2
            )
        )

        self.assertEqual(
            len(translation_coordinators_models),
            2
        )
        self.assertIn(
            self.USER_ID_2,
            translation_coordinators_models[0].coordinator_ids
        )
        self.assertIn(
            self.USER_ID_2,
            translation_coordinators_models[1].coordinator_ids
        )