Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: atlas pull plugins translation | FC-0012 #33922

Merged
merged 1 commit into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,14 @@ endif
push_translations: ## push source strings to Transifex for translation
i18n_tool transifex push

pull_plugin_translations: ## Pull translations from Transifex for edx_django_utils.plugins for both lms and cms
rm -rf conf/plugins-locale/plugins # Clean up existing atlas translations
mkdir -p conf/plugins-locale/plugins
python manage.py lms pull_plugin_translations --verbose $(ATLAS_OPTIONS)
python manage.py lms compile_plugin_translations

pull_xblock_translations: ## pull xblock translations via atlas
rm -rf conf/plugins-locale # Clean up existing atlas translations
rm -rf conf/plugins-locale/xblock.v1 # Clean up existing atlas translations
rm -rf lms/static/i18n/xblock.v1 cms/static/i18n/xblock.v1 # Clean up existing xblock compiled translations
mkdir -p conf/plugins-locale/xblock.v1/ lms/static/js/xblock.v1-i18n cms/static/js
python manage.py lms pull_xblock_translations --verbose $(ATLAS_OPTIONS)
Expand All @@ -76,6 +82,7 @@ ifeq ($(OPENEDX_ATLAS_PULL),)
i18n_tool validate --verbose
else
make pull_xblock_translations
make pull_plugin_translations
find conf/locale -mindepth 1 -maxdepth 1 -type d -exec rm -r {} \;
atlas pull $(ATLAS_OPTIONS) translations/edx-platform/conf/locale:conf/locale
i18n_tool generate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
Download the translations via atlas for the XBlocks.
"""

from django.core.management.base import BaseCommand, CommandError

from openedx.core.djangoapps.plugins.i18n_api import ATLAS_ARGUMENTS
from openedx.core.djangoapps.plugins.i18n_api import BaseAtlasPullCommand
from xmodule.modulestore import api as xmodule_api

from ...translation import xblocks_atlas_pull


class Command(BaseCommand):
class Command(BaseAtlasPullCommand):
"""
Pull the XBlock translations via atlas for the XBlocks.

Expand All @@ -19,33 +17,9 @@ class Command(BaseCommand):
- https://github.com/openedx/openedx-atlas
"""

def add_arguments(self, parser):
for argument in ATLAS_ARGUMENTS:
parser.add_argument(*argument.get_args(), **argument.get_kwargs())

parser.add_argument(
'--verbose|-v',
action='store_true',
default=False,
dest='verbose',
help='Verbose output using `--verbose` argument for `atlas pull`.',
)

def handle(self, *args, **options):
xblock_translations_root = xmodule_api.get_python_locale_root()
feanil marked this conversation as resolved.
Show resolved Hide resolved
if list(xblock_translations_root.listdir()):
raise CommandError(f'"{xblock_translations_root}" should be empty before running atlas pull.')

atlas_pull_options = []

for argument in ATLAS_ARGUMENTS:
option_value = options.get(argument.dest)
if option_value is not None:
atlas_pull_options += [argument.flag, option_value]

if options['verbose']:
atlas_pull_options += ['--verbose']
else:
atlas_pull_options += ['--silent']
self.ensure_empty_directory(xblock_translations_root)

atlas_pull_options = self.get_atlas_pull_options(**options)
xblocks_atlas_pull(pull_options=atlas_pull_options)
Empty file.
14 changes: 12 additions & 2 deletions openedx/core/djangoapps/plugins/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from django.conf import settings
from edx_django_utils.plugins import connect_plugin_receivers

from openedx.core.djangoapps.plugins.constants import ProjectType
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
from edx_django_utils.plugins import PluginSettings


class PluginsConfig(AppConfig):
Expand All @@ -19,7 +20,16 @@ class PluginsConfig(AppConfig):

name = 'openedx.core.djangoapps.plugins'

plugin_app = {}
plugin_app = {
PluginSettings.CONFIG: {
ProjectType.LMS: {
SettingsType.PRODUCTION: {PluginSettings.RELATIVE_PATH: 'settings.production'},
},
ProjectType.CMS: {
SettingsType.PRODUCTION: {PluginSettings.RELATIVE_PATH: 'settings.production'},
},
}
}

def ready(self):
"""
Expand Down
4 changes: 4 additions & 0 deletions openedx/core/djangoapps/plugins/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ class SettingsType():
COMMON = 'common'
DEVSTACK = 'devstack'
TEST = 'test'


# Locale root for IDA plugins for LMS and CMS, relative to settings.REPO_ROOT
plugins_locale_root = 'conf/plugins-locale/plugins'
97 changes: 95 additions & 2 deletions openedx/core/djangoapps/plugins/i18n_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"""

from dataclasses import dataclass, asdict
from collections import defaultdict
import os
from pathlib import Path
import subprocess

from django.core.management import BaseCommand, CommandError
from importlib_metadata import entry_points


@dataclass
class ArgparseArgument:
Expand All @@ -33,7 +37,7 @@ def get_args(self):
return [self.flag]


# `atlas pull` arguments defintions.
# `atlas pull` arguments definitions.
#
# - https://github.com/openedx/openedx-atlas
#
Expand All @@ -53,11 +57,63 @@ def get_args(self):
ArgparseArgument(
flag='--branch',
dest='branch',
help='Custom branch for "atlas pull" e.g. --branch=release/redwood . Default is "main".',
help='Deprecated option. Use --revision instead.',
),
ArgparseArgument(
flag='--revision',
dest='revision',
help='Custom git revision for "atlas pull" e.g. --revision=release/redwood . Default is "main".',
),
]


class BaseAtlasPullCommand(BaseCommand):
"""
Base `atlas pull` Django command.
"""

def add_arguments(self, parser):
"""
Configure Django command arguments.
"""
for argument in ATLAS_ARGUMENTS:
parser.add_argument(*argument.get_args(), **argument.get_kwargs())

parser.add_argument(
'--verbose|-v',
action='store_true',
default=False,
dest='verbose',
help='Verbose output using `--verbose` argument for `atlas pull`.',
)

def ensure_empty_directory(self, directory):
"""
Ensure the pull directory is empty before running atlas pull.
"""
plugin_translations_root = directory
if os.listdir(plugin_translations_root):
raise CommandError(f'"{plugin_translations_root}" should be empty before running atlas pull.')

def get_atlas_pull_options(self, **options):
"""
Pass-through the Django command options to `atlas pull`.
"""
atlas_pull_options = []

for argument in ATLAS_ARGUMENTS:
option_value = options.get(argument.dest)
if option_value is not None:
atlas_pull_options += [argument.flag, option_value]

if options['verbose']:
atlas_pull_options += ['--verbose']
else:
atlas_pull_options += ['--silent']

return atlas_pull_options


def atlas_pull_by_modules(module_names, locale_root, pull_options):
"""
Atlas pull translations by module name instead of repository name.
Expand Down Expand Up @@ -91,3 +147,40 @@ def compile_po_files(root_dir):
args=['msgfmt', '--check-format', '-o', str(po_file_path.with_suffix('.mo')), str(po_file_path)],
check=True,
)


def get_installed_plugins_module_names():
"""
Return the installed plugins Python module names.

This function excludes the built-in edx-platform plugins such as `lms`, `cms` and `openedx`.
"""
# group (e.g 'lms.djangoapp') -> set for root module names (e.g {'edx_sga'})
root_modules = defaultdict(set)

for entry_point in entry_points():
module_name = entry_point.value
root_module = module_name.split('.')[0] # e.g. `edx_sga` from `edx_sga.core.xblock`
root_modules[entry_point.group].add(root_module)

return (
# Return all lms.djangopapp and cms.djangoapp plugins
(root_modules['lms.djangoapp'] | root_modules['cms.djangoapp'])
# excluding the edx-platform built-in plugins which don't need atlas
- {'lms', 'cms', 'common', 'openedx', 'xmodule'}
# excluding XBlocks, which is handled by `pull_xblock_translations` command
- root_modules['xblock.v1']
)


def plugin_translations_atlas_pull(pull_options, locale_root):
"""
Atlas pull the translations for the installed non-XBlocks plugins.
"""
module_names = get_installed_plugins_module_names()

atlas_pull_by_modules(
module_names=module_names,
locale_root=locale_root,
pull_options=pull_options,
)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Compile the translation files for the edx_django_utils.plugins.
"""

from django.core.management.base import BaseCommand
from django.conf import settings


from ...constants import plugins_locale_root

from ... import i18n_api


class Command(BaseCommand):
"""
Compile the translation files for the edx_django_utils.plugins.
"""
def handle(self, *args, **options):
i18n_api.compile_po_files(settings.REPO_ROOT / plugins_locale_root)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Download the translations via atlas for the edx-platform plugins (edx_django_utils.plugins).

For the XBlock command check the `pull_xblock_translations` command.
"""

from django.conf import settings

from openedx.core.djangoapps.plugins.i18n_api import BaseAtlasPullCommand

from ...constants import plugins_locale_root

from ...i18n_api import (
plugin_translations_atlas_pull,
)


class Command(BaseAtlasPullCommand):
"""
Pull the edx_django_utils.plugins translations via atlas.

For detailed information about atlas pull options check the atlas documentation:

- https://github.com/openedx/openedx-atlas
"""

def handle(self, *args, **options):
plugin_translations_root = settings.REPO_ROOT / plugins_locale_root
self.ensure_empty_directory(plugin_translations_root)

atlas_pull_options = self.get_atlas_pull_options(**options)

plugin_translations_atlas_pull(
pull_options=atlas_pull_options,
locale_root=plugin_translations_root,
)
Empty file.
17 changes: 17 additions & 0 deletions openedx/core/djangoapps/plugins/settings/production.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Production environment variables for `edx_django_utils.plugins` plugins.
"""

from ..constants import plugins_locale_root


def plugin_settings(settings):
"""
Settings for the `edx_django_utils.plugins` plugins.
"""
locale_root = settings.REPO_ROOT / plugins_locale_root
if locale_root.isdir():
for plugin_locale in locale_root.listdir():
# Add the plugin locale directory only if it's a non-empty directory
if plugin_locale.isdir() and plugin_locale.listdir():
settings.LOCALE_PATHS.append(plugin_locale)
49 changes: 49 additions & 0 deletions openedx/core/djangoapps/plugins/tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Tests for the plugins.i18n_api Django commands module.
"""
from unittest.mock import patch

from django.core.management import call_command


def test_pull_plugin_translations_command(settings, tmp_path):
"""
Test the `pull_plugin_translations` Django command.
"""
plugins_locale_root = tmp_path / 'conf/plugins-locale/plugins'
plugins_locale_root.mkdir(parents=True)
settings.REPO_ROOT = tmp_path

with patch('subprocess.run') as mock_run:
call_command(
'pull_plugin_translations',
verbose=True,
filter='ar,es_ES',
repository='custom_repo',
)

assert mock_run.call_count == 1, 'Expected to call `subprocess.run` once'
call_kwargs = mock_run.call_args.kwargs

assert call_kwargs['check'] is True
assert call_kwargs['cwd'] == plugins_locale_root
assert call_kwargs['args'][:8] == [
'atlas', 'pull', '--expand-glob',
'--filter', 'ar,es_ES',
'--repository', 'custom_repo',
'--verbose'
], 'Pass arguments to atlas pull correctly'

assert 'translations/*/edx_proctoring/conf/locale:edx_proctoring' in call_kwargs['args'], (
'Pull edx-proctoring translations by Python module name using the "--expand-glob" option'
)


def test_compile_plugin_translations_command(settings):
"""
Test the `compile_plugin_translations` Django command.
"""
with patch('openedx.core.djangoapps.plugins.i18n_api.compile_po_files') as mock_compile_po_files:
call_command('compile_plugin_translations')

mock_compile_po_files.assert_called_once_with(settings.REPO_ROOT / 'conf/plugins-locale/plugins')
Loading
Loading