Skip to content

Commit

Permalink
Merge pull request #33922 from Zeit-Labs/plugins-i18n
Browse files Browse the repository at this point in the history
feat: atlas pull plugins translation | FC-0012
  • Loading branch information
Feanil Patel authored Feb 1, 2024
2 parents 09493a8 + 867eeff commit e6333d0
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 36 deletions.
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()
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

0 comments on commit e6333d0

Please sign in to comment.