Skip to content

Commit

Permalink
Azure service admin controls for automatic voiceover (oppia#21453)
Browse files Browse the repository at this point in the history
* Adds admin controls for Azure config

* Fixes mypy checks

* Updates variable name

* Fixes backend tests
  • Loading branch information
Nik-09 authored Jan 6, 2025
1 parent 53bb6ce commit 767159c
Show file tree
Hide file tree
Showing 14 changed files with 450 additions and 10 deletions.
54 changes: 54 additions & 0 deletions core/controllers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from core.domain import topic_services
from core.domain import translation_domain
from core.domain import user_services
from core.domain import voiceover_services
from core.domain import wipeout_service

from typing import Dict, List, Optional, TypedDict, Union, cast
Expand Down Expand Up @@ -2243,6 +2244,59 @@ def get(self) -> None:
)


class AutomaticVoiceoverAdminControlHandlerNormalizedPayloadDict(TypedDict):
"""Dict representation of AutomaticVoiceoverAdminControlHandler's
normalized_payload dictionary.
"""

autogenerated_voiceovers_are_enabled: bool


class AutomaticVoiceoverAdminControlHandler(
base.BaseHandler[
AutomaticVoiceoverAdminControlHandlerNormalizedPayloadDict,
Dict[str, str]
]
):
"""Retrieves and updates automatic voiceover admin control."""

GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
URL_PATH_ARGS_SCHEMAS: Dict[str, str] = {}
HANDLER_ARGS_SCHEMAS = {
'GET': {},
'POST': {
'autogenerated_voiceovers_are_enabled': {
'schema': {
'type': 'bool'
},
}
}
}

@acl_decorators.open_access
def get(self) -> None:
"""Retrieves the admin config data for automatic voiceovers."""
self.render_json({
'autogenerated_voiceovers_are_enabled': (
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)
})

@acl_decorators.can_access_admin_page
def post(self) -> None:
"""Updates the admin config data for automatic voiceovers."""
assert self.normalized_payload is not None
autogenerated_voiceovers_are_enabled: bool = (
self.normalized_payload[
'autogenerated_voiceovers_are_enabled'])
assert isinstance(autogenerated_voiceovers_are_enabled, bool)

voiceover_services.update_admin_config_for_voiceover_autogeneration(
autogenerated_voiceovers_are_enabled)
self.render_json({})


class DataExtractionQueryHandlerNormalizedRequestDict(TypedDict):
"""Dict representation of DataExtractionQueryHandler's
normalized_request dictionary.
Expand Down
79 changes: 75 additions & 4 deletions core/controllers/admin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from core.domain import topic_fetchers
from core.domain import topic_services
from core.domain import user_services
from core.domain import voiceover_services
from core.domain import wipeout_service
from core.platform import models
from core.platform.auth import firebase_auth_services
Expand All @@ -62,13 +63,15 @@
from mypy_imports import exp_models
from mypy_imports import opportunity_models
from mypy_imports import user_models
from mypy_imports import voiceover_models

(
audit_models, blog_models, exp_models, opportunity_models,
user_models
user_models, voiceover_models
) = models.Registry.import_models([
models.Names.AUDIT, models.Names.BLOG, models.Names.EXPLORATION,
models.Names.OPPORTUNITY, models.Names.USER
models.Names.OPPORTUNITY, models.Names.USER,
models.Names.VOICEOVER
])

BOTH_MODERATOR_AND_ADMIN_EMAIL = '[email protected]'
Expand Down Expand Up @@ -1499,7 +1502,7 @@ def test_raises_error_if_not_question_admin_or_question_submitter_(# pylint: dis


class GenerateDummyTranslationOpportunitiesTest(test_utils.GenericTestBase):
"""Checks the conditions for generation of dummy translation
"""Checks the conditions for generation of dummy translation
opportunities."""

def setUp(self) -> None:
Expand Down Expand Up @@ -1548,7 +1551,7 @@ def test_handler_raises_error_with_non_int_num_dummy_translation_opportunities_t
response = self.post_json(
'/adminhandler', {
'action': 'generate_dummy_translation_opportunities',
'num_dummy_translation_opportunities_to_generate':
'num_dummy_translation_opportunities_to_generate':
'invalid_type'
}, csrf_token=csrf_token, expected_status_int=400)

Expand Down Expand Up @@ -3367,3 +3370,71 @@ def test_handler_with_without_exploration_id_in_payload_raise_error(self) -> Non
'At \'http://localhost/interactions\' these errors are happening:\n'
'Missing key in handler args: exp_id.'
)


class AutomaticVoiceoverAdminControlHandlerTests(test_utils.GenericTestBase):
"""Tests for admin config for automatic voiceovers."""

def setUp(self) -> None:
super().setUp()
self.signup(self.CURRICULUM_ADMIN_EMAIL, 'testsuper')
self.voiceover_autogeneration_policy_model = (
voiceover_models.VoiceoverAutogenerationPolicyModel(
id=voiceover_models.VOICEOVER_AUTOGENERATION_POLICY_ID)
)
self.voiceover_autogeneration_policy_model.language_codes_mapping = {}
(
self.voiceover_autogeneration_policy_model.
autogenerated_voiceovers_are_enabled
) = True
self.voiceover_autogeneration_policy_model.update_timestamps()
self.voiceover_autogeneration_policy_model.put()

def test_handler_should_be_able_to_return_azure_config(self) -> None:
response = self.get_json(
'/automatic_voiceover_admin_control_handler',
expected_status_int=200)
self.assertTrue(
response['autogenerated_voiceovers_are_enabled'])

def test_admin_should_be_able_to_update_config(self) -> None:
self.login(self.CURRICULUM_ADMIN_EMAIL, is_super_admin=True)
csrf_token = self.get_new_csrf_token()

voiceover_services.update_admin_config_for_voiceover_autogeneration(
False)
self.assertFalse(
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)

self.post_json(
'/automatic_voiceover_admin_control_handler', {
'autogenerated_voiceovers_are_enabled': True
}, csrf_token=csrf_token)

self.assertTrue(
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)
self.logout()

def test_non_admin_should_not_be_able_to_update_config(self) -> None:
csrf_token = self.get_new_csrf_token()

voiceover_services.update_admin_config_for_voiceover_autogeneration(
False)
self.assertFalse(
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)

self.post_json(
'/automatic_voiceover_admin_control_handler', {
'autogenerated_voiceovers_are_enabled': 'true'
}, csrf_token=csrf_token, expected_status_int=400)

self.assertFalse(
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)
51 changes: 51 additions & 0 deletions core/domain/voiceover_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,57 @@ def get_all_language_accent_codes_for_voiceovers(
return language_codes_mapping


def is_voiceover_autogeneration_using_cloud_service_enabled() -> bool:
"""The method verifies whether admins have enabled the configuration
for generating voiceovers automatically using cloud service.
Returns:
bool. True if cloud based voiceover autogeneration is enabled,
False otherwise.
"""
voiceover_autogeneration_policy_model = (
voiceover_models.VoiceoverAutogenerationPolicyModel.get(
voiceover_models.VOICEOVER_AUTOGENERATION_POLICY_ID, strict=False)
)

autogenerated_voiceovers_are_enabled: bool = False

if voiceover_autogeneration_policy_model is not None:
autogenerated_voiceovers_are_enabled = (
voiceover_autogeneration_policy_model.
autogenerated_voiceovers_are_enabled
)
return autogenerated_voiceovers_are_enabled


def update_admin_config_for_voiceover_autogeneration(
autogenerated_voiceovers_are_enabled: bool
) -> None:
"""The method allows admins to enable or disable the use of cloud service
for automatic voiceover generation from admin misc tab.
Args:
autogenerated_voiceovers_are_enabled: bool. A boolean value
indicating whether cloud based voiceover autogeneration is enabled
by admins or not.
"""
voiceover_autogeneration_policy_model = (
voiceover_models.VoiceoverAutogenerationPolicyModel.get(
voiceover_models.VOICEOVER_AUTOGENERATION_POLICY_ID, strict=False))

assert voiceover_autogeneration_policy_model is not None

(
voiceover_autogeneration_policy_model.
autogenerated_voiceovers_are_enabled
) = (
autogenerated_voiceovers_are_enabled
)

voiceover_autogeneration_policy_model.update_timestamps()
voiceover_autogeneration_policy_model.put()


def create_entity_voiceovers_model(
entity_voiceovers: voiceover_domain.EntityVoiceovers
) -> voiceover_models.EntityVoiceoversModel:
Expand Down
26 changes: 26 additions & 0 deletions core/domain/voiceover_services_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,32 @@ def test_save_and_get_language_accent_codes_works_correctly(self) -> None:
self.assertDictEqual(
retrieved_language_codes_mapping, language_codes_mapping)

def test_get_and_set_azure_config_for_automatic_voiceovers(self) -> None:
voiceover_autogeneration_policy_model = (
voiceover_models.VoiceoverAutogenerationPolicyModel(
id=voiceover_models.VOICEOVER_AUTOGENERATION_POLICY_ID)
)
voiceover_autogeneration_policy_model.language_codes_mapping = {}
(
voiceover_autogeneration_policy_model.
autogenerated_voiceovers_are_enabled
) = True
voiceover_autogeneration_policy_model.update_timestamps()
voiceover_autogeneration_policy_model.put()

self.assertTrue(
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)

voiceover_services.update_admin_config_for_voiceover_autogeneration(
False)

self.assertFalse(
voiceover_services.
is_voiceover_autogeneration_using_cloud_service_enabled()
)


class VoiceoversLanguageAccentConstantsTests(test_utils.GenericTestBase):
"""Unit tests to validate the language-accent information saved as
Expand Down
2 changes: 2 additions & 0 deletions core/feconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ def get_empty_ratings() -> Dict[str, int]:
ADMIN_ROLE_HANDLER_URL = '/adminrolehandler'
BLOG_ADMIN_ROLE_HANDLER_URL = '/blogadminrolehandler'
BLOG_DASHBOARD_DATA_URL = '/blogdashboardhandler/data'
AUTOMATIC_VOICEOVER_ADMIN_CONTROL_URL = (
'/automatic_voiceover_admin_control_handler')
DIAGNOSTIC_TEST_PLAYER_PAGE_URL = '/diagnostic-test-player'
BLOG_EDITOR_DATA_URL_PREFIX = '/blogeditorhandler/data'
BULK_EMAIL_WEBHOOK_ENDPOINT = '/bulk_email_webhook_endpoint'
Expand Down
10 changes: 9 additions & 1 deletion core/storage/voiceover/gae_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ class VoiceoverAutogenerationPolicyModel(base_models.BaseModel):
# for this language-accent code as values.
language_codes_mapping = datastore_services.JsonProperty(required=True)

# Boolean flag indicating whether cloud based voiceover autogeneration is
# enabled.
autogenerated_voiceovers_are_enabled = (
datastore_services.BooleanProperty(required=True, default=False)
)

@staticmethod
def get_deletion_policy() -> base_models.DELETION_POLICY:
"""Model doesn't contain any data directly corresponding to a user."""
Expand All @@ -228,7 +234,9 @@ def get_model_association_to_user(
def get_export_policy(cls) -> Dict[str, base_models.EXPORT_POLICY]:
"""Model doesn't contain any data directly corresponding to a user."""
return dict(super(cls, cls).get_export_policy(), **{
'language_codes_mapping': base_models.EXPORT_POLICY.NOT_APPLICABLE
'language_codes_mapping': base_models.EXPORT_POLICY.NOT_APPLICABLE,
'autogenerated_voiceovers_are_enabled': (
base_models.EXPORT_POLICY.NOT_APPLICABLE)
})


Expand Down
2 changes: 2 additions & 0 deletions core/storage/voiceover/gae_models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ def test_get_export_policy_not_applicable(self) -> None:
'deleted': base_models.EXPORT_POLICY.NOT_APPLICABLE,
'last_updated': base_models.EXPORT_POLICY.NOT_APPLICABLE,
'language_codes_mapping': (
base_models.EXPORT_POLICY.NOT_APPLICABLE),
'autogenerated_voiceovers_are_enabled': (
base_models.EXPORT_POLICY.NOT_APPLICABLE)
}
)
Expand Down
Loading

0 comments on commit 767159c

Please sign in to comment.