ol-openedx-auto-select-language 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ol_openedx_auto_select_language-0.1.0/.gitignore +14 -0
- ol_openedx_auto_select_language-0.1.0/LICENSE.txt +28 -0
- ol_openedx_auto_select_language-0.1.0/PKG-INFO +94 -0
- ol_openedx_auto_select_language-0.1.0/README.rst +80 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/__init__.py +3 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/apps.py +34 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/constants.py +3 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/filters.py +58 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/middleware.py +145 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/settings/cms.py +17 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/settings/common.py +14 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/settings/lms.py +38 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/urls.py +23 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/utils.py +96 -0
- ol_openedx_auto_select_language-0.1.0/ol_openedx_auto_select_language/views.py +76 -0
- ol_openedx_auto_select_language-0.1.0/pyproject.toml +39 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (C) 2022 MIT Open Learning
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
* Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ol-openedx-auto-select-language
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An Open edX plugin to auto-select the language based on course language settings.
|
|
5
|
+
Author: MIT Office of Digital Learning
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE.txt
|
|
8
|
+
Keywords: Python,edx
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: django>=4.0
|
|
11
|
+
Requires-Dist: djangorestframework>=3.14.0
|
|
12
|
+
Requires-Dist: edx-opaque-keys
|
|
13
|
+
Description-Content-Type: text/x-rst
|
|
14
|
+
|
|
15
|
+
OL Open edX Auto Select Language
|
|
16
|
+
================================
|
|
17
|
+
|
|
18
|
+
An Open edX plugin to auto select the Open edX platform language based on the course language.
|
|
19
|
+
|
|
20
|
+
Purpose
|
|
21
|
+
*******
|
|
22
|
+
|
|
23
|
+
Auto select the Open edX platform language based on the course language. When enabled, users will see the static site content in the course's configured language.
|
|
24
|
+
|
|
25
|
+
Setup
|
|
26
|
+
=====
|
|
27
|
+
|
|
28
|
+
For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_.
|
|
29
|
+
|
|
30
|
+
Installation required in:
|
|
31
|
+
|
|
32
|
+
* Studio (CMS)
|
|
33
|
+
* LMS
|
|
34
|
+
|
|
35
|
+
Configuration
|
|
36
|
+
=============
|
|
37
|
+
|
|
38
|
+
- Add the following configuration values to the config file in Open edX. For any release after Juniper, that config file is ``/edx/etc/lms.yml`` and ``/edx/etc/cms.yml``. If you're using ``private.py``, add these values to ``lms/envs/private.py`` and ``cms/envs/private.py``. These should be added to the top level. **Ask a fellow developer for these values.**
|
|
39
|
+
|
|
40
|
+
.. code-block:: python
|
|
41
|
+
|
|
42
|
+
# Enable auto language selection
|
|
43
|
+
ENABLE_AUTO_LANGUAGE_SELECTION: true
|
|
44
|
+
|
|
45
|
+
- For Tutor installations, these values can also be managed through a `custom Tutor plugin <https://docs.tutor.edly.io/tutorials/plugin.html#plugin-development-tutorial>`_.
|
|
46
|
+
|
|
47
|
+
Auto Language Selection
|
|
48
|
+
=======================
|
|
49
|
+
|
|
50
|
+
The plugin includes an auto language selection feature that automatically sets the user's language preference based on the course language. When enabled, users will see the static site content in the course's configured language.
|
|
51
|
+
|
|
52
|
+
To enable auto language selection:
|
|
53
|
+
|
|
54
|
+
1. Set ``ENABLE_AUTO_LANGUAGE_SELECTION`` to ``true`` in your settings.
|
|
55
|
+
|
|
56
|
+
2. Set ``SHARED_COOKIE_DOMAIN`` to your domain (e.g., ``.local.openedx.io`` for local tutor setup) to allow cookies to be shared between LMS and CMS.
|
|
57
|
+
|
|
58
|
+
**How it works:**
|
|
59
|
+
|
|
60
|
+
- **LMS**: The ``CourseLanguageCookieMiddleware`` automatically detects course URLs and sets the language preference based on the course's configured language.
|
|
61
|
+
- **CMS**: The ``CourseLanguageCookieResetMiddleware`` ensures Studio always uses English for the authoring interface.
|
|
62
|
+
- **Admin areas**: Admin URLs (``/admin``, ``/sysadmin``, instructor dashboards) are forced to use English regardless of course language.
|
|
63
|
+
|
|
64
|
+
MFE Integration
|
|
65
|
+
===============
|
|
66
|
+
|
|
67
|
+
To make auto language selection work with Micro-Frontends (MFEs), you need to use a custom Footer component that handles language detection and switching.
|
|
68
|
+
|
|
69
|
+
**Setup:**
|
|
70
|
+
|
|
71
|
+
1. Use the Footer component from `src/bridge/settings/openedx/mfe/slot_config/Footer.jsx <https://github.com/mitodl/ol-infrastructure/blob/main/src/bridge/settings/openedx/mfe/slot_config/Footer.jsx>`_ in the `ol-infrastructure <https://github.com/mitodl/ol-infrastructure>`_ repository.
|
|
72
|
+
|
|
73
|
+
2. Enable auto language selection in each MFE by adding the following to their ``.env.development`` file:
|
|
74
|
+
|
|
75
|
+
.. code-block:: bash
|
|
76
|
+
|
|
77
|
+
ENABLE_AUTO_LANGUAGE_SELECTION="true"
|
|
78
|
+
|
|
79
|
+
3. This custom Footer component:
|
|
80
|
+
- Detects the current course context in MFEs
|
|
81
|
+
- Automatically switches the MFE language based on the course's configured language
|
|
82
|
+
- Ensures consistent language experience across the platform
|
|
83
|
+
|
|
84
|
+
4. Configure your MFE slot overrides to use this custom Footer component instead of the default one.
|
|
85
|
+
|
|
86
|
+
**Note:** The custom Footer is required because MFEs run as separate applications and need their own mechanism to detect and respond to course language settings. The environment variable must be set in each MFE's configuration for the feature to work properly.
|
|
87
|
+
|
|
88
|
+
License
|
|
89
|
+
*******
|
|
90
|
+
|
|
91
|
+
The code in this repository is licensed under the AGPL 3.0 unless
|
|
92
|
+
otherwise noted.
|
|
93
|
+
|
|
94
|
+
Please see `LICENSE.txt <LICENSE.txt>`_ for details.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
OL Open edX Auto Select Language
|
|
2
|
+
================================
|
|
3
|
+
|
|
4
|
+
An Open edX plugin to auto select the Open edX platform language based on the course language.
|
|
5
|
+
|
|
6
|
+
Purpose
|
|
7
|
+
*******
|
|
8
|
+
|
|
9
|
+
Auto select the Open edX platform language based on the course language. When enabled, users will see the static site content in the course's configured language.
|
|
10
|
+
|
|
11
|
+
Setup
|
|
12
|
+
=====
|
|
13
|
+
|
|
14
|
+
For detailed installation instructions, please refer to the `plugin installation guide <../../docs#installation-guide>`_.
|
|
15
|
+
|
|
16
|
+
Installation required in:
|
|
17
|
+
|
|
18
|
+
* Studio (CMS)
|
|
19
|
+
* LMS
|
|
20
|
+
|
|
21
|
+
Configuration
|
|
22
|
+
=============
|
|
23
|
+
|
|
24
|
+
- Add the following configuration values to the config file in Open edX. For any release after Juniper, that config file is ``/edx/etc/lms.yml`` and ``/edx/etc/cms.yml``. If you're using ``private.py``, add these values to ``lms/envs/private.py`` and ``cms/envs/private.py``. These should be added to the top level. **Ask a fellow developer for these values.**
|
|
25
|
+
|
|
26
|
+
.. code-block:: python
|
|
27
|
+
|
|
28
|
+
# Enable auto language selection
|
|
29
|
+
ENABLE_AUTO_LANGUAGE_SELECTION: true
|
|
30
|
+
|
|
31
|
+
- For Tutor installations, these values can also be managed through a `custom Tutor plugin <https://docs.tutor.edly.io/tutorials/plugin.html#plugin-development-tutorial>`_.
|
|
32
|
+
|
|
33
|
+
Auto Language Selection
|
|
34
|
+
=======================
|
|
35
|
+
|
|
36
|
+
The plugin includes an auto language selection feature that automatically sets the user's language preference based on the course language. When enabled, users will see the static site content in the course's configured language.
|
|
37
|
+
|
|
38
|
+
To enable auto language selection:
|
|
39
|
+
|
|
40
|
+
1. Set ``ENABLE_AUTO_LANGUAGE_SELECTION`` to ``true`` in your settings.
|
|
41
|
+
|
|
42
|
+
2. Set ``SHARED_COOKIE_DOMAIN`` to your domain (e.g., ``.local.openedx.io`` for local tutor setup) to allow cookies to be shared between LMS and CMS.
|
|
43
|
+
|
|
44
|
+
**How it works:**
|
|
45
|
+
|
|
46
|
+
- **LMS**: The ``CourseLanguageCookieMiddleware`` automatically detects course URLs and sets the language preference based on the course's configured language.
|
|
47
|
+
- **CMS**: The ``CourseLanguageCookieResetMiddleware`` ensures Studio always uses English for the authoring interface.
|
|
48
|
+
- **Admin areas**: Admin URLs (``/admin``, ``/sysadmin``, instructor dashboards) are forced to use English regardless of course language.
|
|
49
|
+
|
|
50
|
+
MFE Integration
|
|
51
|
+
===============
|
|
52
|
+
|
|
53
|
+
To make auto language selection work with Micro-Frontends (MFEs), you need to use a custom Footer component that handles language detection and switching.
|
|
54
|
+
|
|
55
|
+
**Setup:**
|
|
56
|
+
|
|
57
|
+
1. Use the Footer component from `src/bridge/settings/openedx/mfe/slot_config/Footer.jsx <https://github.com/mitodl/ol-infrastructure/blob/main/src/bridge/settings/openedx/mfe/slot_config/Footer.jsx>`_ in the `ol-infrastructure <https://github.com/mitodl/ol-infrastructure>`_ repository.
|
|
58
|
+
|
|
59
|
+
2. Enable auto language selection in each MFE by adding the following to their ``.env.development`` file:
|
|
60
|
+
|
|
61
|
+
.. code-block:: bash
|
|
62
|
+
|
|
63
|
+
ENABLE_AUTO_LANGUAGE_SELECTION="true"
|
|
64
|
+
|
|
65
|
+
3. This custom Footer component:
|
|
66
|
+
- Detects the current course context in MFEs
|
|
67
|
+
- Automatically switches the MFE language based on the course's configured language
|
|
68
|
+
- Ensures consistent language experience across the platform
|
|
69
|
+
|
|
70
|
+
4. Configure your MFE slot overrides to use this custom Footer component instead of the default one.
|
|
71
|
+
|
|
72
|
+
**Note:** The custom Footer is required because MFEs run as separate applications and need their own mechanism to detect and respond to course language settings. The environment variable must be set in each MFE's configuration for the feature to work properly.
|
|
73
|
+
|
|
74
|
+
License
|
|
75
|
+
*******
|
|
76
|
+
|
|
77
|
+
The code in this repository is licensed under the AGPL 3.0 unless
|
|
78
|
+
otherwise noted.
|
|
79
|
+
|
|
80
|
+
Please see `LICENSE.txt <LICENSE.txt>`_ for details.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ol_openedx_auto_select_language Django application initialization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from edx_django_utils.plugins import PluginSettings, PluginURLs
|
|
7
|
+
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OLOpenEdxAutoSelectLanguageConfig(AppConfig):
|
|
11
|
+
"""
|
|
12
|
+
Configuration for the ol_openedx_auto_select_language Django application.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "ol_openedx_auto_select_language"
|
|
16
|
+
verbose_name = "OL Auto Select Language"
|
|
17
|
+
|
|
18
|
+
plugin_app = {
|
|
19
|
+
PluginURLs.CONFIG: {
|
|
20
|
+
ProjectType.LMS: {
|
|
21
|
+
PluginURLs.NAMESPACE: "",
|
|
22
|
+
PluginURLs.REGEX: "",
|
|
23
|
+
PluginURLs.RELATIVE_PATH: "urls",
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
PluginSettings.CONFIG: {
|
|
27
|
+
ProjectType.CMS: {
|
|
28
|
+
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.cms"},
|
|
29
|
+
},
|
|
30
|
+
ProjectType.LMS: {
|
|
31
|
+
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.lms"},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filters for Open edX auto select language.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from openedx_filters import PipelineStep
|
|
6
|
+
from xmodule.modulestore.django import modulestore
|
|
7
|
+
|
|
8
|
+
from ol_openedx_auto_select_language.constants import (
|
|
9
|
+
ENGLISH_LANGUAGE_CODE,
|
|
10
|
+
)
|
|
11
|
+
from ol_openedx_auto_select_language.utils import LanguageCode
|
|
12
|
+
|
|
13
|
+
VIDEO_BLOCK_TYPE = "video"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AddDestLangForVideoBlock(PipelineStep):
|
|
17
|
+
"""
|
|
18
|
+
Pipeline step to add destination language for video transcripts
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def run_filter(self, context, student_view_context):
|
|
22
|
+
"""
|
|
23
|
+
Add the destination language to the student view context if a video block
|
|
24
|
+
with transcripts in the course language is found among the child blocks.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def set_dest_lang_for_video_block(block_key):
|
|
28
|
+
"""
|
|
29
|
+
Set the destination language for video blocks with
|
|
30
|
+
transcripts in the course language.
|
|
31
|
+
"""
|
|
32
|
+
student_view_context["dest_lang"] = (
|
|
33
|
+
ENGLISH_LANGUAGE_CODE # default to English
|
|
34
|
+
)
|
|
35
|
+
video_block = modulestore().get_item(block_key)
|
|
36
|
+
transcripts_info = video_block.get_transcripts_info()
|
|
37
|
+
dest_lang = getattr(
|
|
38
|
+
context.get("course", None), "language", ENGLISH_LANGUAGE_CODE
|
|
39
|
+
)
|
|
40
|
+
dest_lang = LanguageCode(dest_lang).to_bcp47()
|
|
41
|
+
if (
|
|
42
|
+
transcripts_info
|
|
43
|
+
and transcripts_info.get("transcripts", {})
|
|
44
|
+
and dest_lang in transcripts_info["transcripts"]
|
|
45
|
+
):
|
|
46
|
+
student_view_context["dest_lang"] = dest_lang
|
|
47
|
+
|
|
48
|
+
block = dict(context)["block"]
|
|
49
|
+
block_usage_key = block.usage_key
|
|
50
|
+
block_type = block_usage_key.block_type
|
|
51
|
+
if block_type == "vertical":
|
|
52
|
+
for child in getattr(block, "children", []):
|
|
53
|
+
if child.block_type == VIDEO_BLOCK_TYPE:
|
|
54
|
+
set_dest_lang_for_video_block(child)
|
|
55
|
+
elif block_type == VIDEO_BLOCK_TYPE:
|
|
56
|
+
set_dest_lang_for_video_block(block_usage_key)
|
|
57
|
+
|
|
58
|
+
return {"context": context, "student_view_context": student_view_context}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Middleware to set/reset language preference cookie and
|
|
3
|
+
user preference based on course language.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.http import HttpResponseRedirect
|
|
10
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
11
|
+
from opaque_keys.edx.keys import CourseKey
|
|
12
|
+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
13
|
+
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
|
14
|
+
from openedx.core.djangoapps.lang_pref import helpers as lang_pref_helpers
|
|
15
|
+
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
|
16
|
+
|
|
17
|
+
from ol_openedx_auto_select_language.constants import ENGLISH_LANGUAGE_CODE
|
|
18
|
+
from ol_openedx_auto_select_language.utils import LanguageCode
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def should_process_request(request):
|
|
22
|
+
"""
|
|
23
|
+
Return True if language auto-selection should run for this request.
|
|
24
|
+
"""
|
|
25
|
+
return (
|
|
26
|
+
settings.ENABLE_AUTO_LANGUAGE_SELECTION
|
|
27
|
+
and hasattr(request, "user")
|
|
28
|
+
and request.user.is_authenticated
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_language(request, response, language):
|
|
33
|
+
"""
|
|
34
|
+
Set both cookie and user preference for language.
|
|
35
|
+
"""
|
|
36
|
+
lang_pref_helpers.set_language_cookie(request, response, language)
|
|
37
|
+
set_user_preference(request.user, LANGUAGE_KEY, language)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def redirect_current_path(request):
|
|
41
|
+
"""
|
|
42
|
+
Redirect to the same URL to ensure language change takes effect.
|
|
43
|
+
"""
|
|
44
|
+
return HttpResponseRedirect(request.get_full_path())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CourseLanguageCookieMiddleware(MiddlewareMixin):
|
|
48
|
+
"""
|
|
49
|
+
LMS middleware that:
|
|
50
|
+
- Sets language based on course language
|
|
51
|
+
- Forces English for exempt paths and authoring MFEs
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
COURSE_URL_REGEX = re.compile(
|
|
55
|
+
rf"^/courses/(?P<course_key>{settings.COURSE_KEY_REGEX})(?:/|$)",
|
|
56
|
+
re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def process_response(self, request, response):
|
|
60
|
+
"""
|
|
61
|
+
Process the response to set/reset language cookie based on course language.
|
|
62
|
+
"""
|
|
63
|
+
if not should_process_request(request):
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
path = getattr(request, "path_info", request.path)
|
|
67
|
+
|
|
68
|
+
if self._should_force_english(request, path):
|
|
69
|
+
return self._force_english_if_needed(request, response)
|
|
70
|
+
|
|
71
|
+
course_language = self._get_course_language(path)
|
|
72
|
+
if not course_language:
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
return self._apply_course_language(request, response, course_language)
|
|
76
|
+
|
|
77
|
+
def _should_force_english(self, request, path):
|
|
78
|
+
"""
|
|
79
|
+
Determine if English should be forced based on request origin or exempt paths.
|
|
80
|
+
"""
|
|
81
|
+
return request.META.get(
|
|
82
|
+
"HTTP_ORIGIN"
|
|
83
|
+
) == settings.COURSE_AUTHORING_MICROFRONTEND_URL or any(
|
|
84
|
+
exempt_path in path
|
|
85
|
+
for exempt_path in settings.AUTO_LANGUAGE_SELECTION_EXEMPT_PATHS
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _force_english_if_needed(self, request, response):
|
|
89
|
+
"""
|
|
90
|
+
Force language to English if not already set.
|
|
91
|
+
"""
|
|
92
|
+
cookie_val = lang_pref_helpers.get_language_cookie(request)
|
|
93
|
+
|
|
94
|
+
if cookie_val != ENGLISH_LANGUAGE_CODE:
|
|
95
|
+
set_language(request, response, ENGLISH_LANGUAGE_CODE)
|
|
96
|
+
return redirect_current_path(request)
|
|
97
|
+
|
|
98
|
+
return response
|
|
99
|
+
|
|
100
|
+
def _get_course_language(self, path):
|
|
101
|
+
"""
|
|
102
|
+
Extract course language from the course URL path.
|
|
103
|
+
"""
|
|
104
|
+
match = self.COURSE_URL_REGEX.match(path)
|
|
105
|
+
if not match:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
course_key = CourseKey.from_string(match.group("course_key"))
|
|
110
|
+
overview = CourseOverview.get_from_id(course_key)
|
|
111
|
+
except Exception: # noqa: BLE001
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return getattr(overview, "language", None)
|
|
115
|
+
|
|
116
|
+
def _apply_course_language(self, request, response, language):
|
|
117
|
+
"""
|
|
118
|
+
Apply the course language if it differs from the current cookie value.
|
|
119
|
+
"""
|
|
120
|
+
language = LanguageCode(language).to_bcp47()
|
|
121
|
+
cookie_val = lang_pref_helpers.get_language_cookie(request)
|
|
122
|
+
if cookie_val != language:
|
|
123
|
+
set_language(request, response, language)
|
|
124
|
+
return redirect_current_path(request)
|
|
125
|
+
|
|
126
|
+
return response
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class CourseLanguageCookieResetMiddleware(MiddlewareMixin):
|
|
130
|
+
"""
|
|
131
|
+
CMS middleware that always resets language to English.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def process_response(self, request, response):
|
|
135
|
+
"""
|
|
136
|
+
Process the response to reset language cookie to English.
|
|
137
|
+
"""
|
|
138
|
+
if not should_process_request(request):
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
cookie_val = lang_pref_helpers.get_language_cookie(request)
|
|
142
|
+
if cookie_val and cookie_val != ENGLISH_LANGUAGE_CODE:
|
|
143
|
+
set_language(request, response, ENGLISH_LANGUAGE_CODE)
|
|
144
|
+
|
|
145
|
+
return response
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# noqa: INP001
|
|
2
|
+
|
|
3
|
+
"""Settings to provide to edX"""
|
|
4
|
+
|
|
5
|
+
from ol_openedx_auto_select_language.settings.common import apply_common_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def plugin_settings(settings):
|
|
9
|
+
"""
|
|
10
|
+
Populate cms settings
|
|
11
|
+
"""
|
|
12
|
+
apply_common_settings(settings)
|
|
13
|
+
settings.MIDDLEWARE.extend(
|
|
14
|
+
[
|
|
15
|
+
"ol_openedx_auto_select_language.middleware.CourseLanguageCookieResetMiddleware"
|
|
16
|
+
]
|
|
17
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# noqa: INP001
|
|
2
|
+
|
|
3
|
+
"""Common settings for LMS and CMS to provide to edX"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def apply_common_settings(settings):
|
|
7
|
+
"""
|
|
8
|
+
Apply custom settings function for LMS and CMS settings.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
settings: Django settings object to modify
|
|
12
|
+
"""
|
|
13
|
+
settings.ENABLE_AUTO_LANGUAGE_SELECTION = False
|
|
14
|
+
settings.AUTO_LANGUAGE_SELECTION_EXEMPT_PATHS = ["admin", "sysadmin", "instructor"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# noqa: INP001
|
|
2
|
+
|
|
3
|
+
"""Settings to provide to edX"""
|
|
4
|
+
|
|
5
|
+
from ol_openedx_auto_select_language.settings.common import apply_common_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def plugin_settings(settings):
|
|
9
|
+
"""
|
|
10
|
+
Populate lms settings
|
|
11
|
+
"""
|
|
12
|
+
apply_common_settings(settings)
|
|
13
|
+
settings.MIDDLEWARE.extend(
|
|
14
|
+
["ol_openedx_auto_select_language.middleware.CourseLanguageCookieMiddleware"]
|
|
15
|
+
)
|
|
16
|
+
VIDEO_TRANSCRIPT_LANGUAGE_FILTERS = {
|
|
17
|
+
"org.openedx.learning.xblock.render.started.v1": {
|
|
18
|
+
"pipeline": [
|
|
19
|
+
"ol_openedx_auto_select_language.filters.AddDestLangForVideoBlock"
|
|
20
|
+
],
|
|
21
|
+
"fail_silently": False,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
existing_filters = getattr(settings, "OPEN_EDX_FILTERS_CONFIG", {})
|
|
25
|
+
|
|
26
|
+
# Merge pipeline lists instead of overwriting
|
|
27
|
+
for filter_name, config in VIDEO_TRANSCRIPT_LANGUAGE_FILTERS.items():
|
|
28
|
+
if filter_name not in existing_filters:
|
|
29
|
+
existing_filters[filter_name] = config
|
|
30
|
+
else:
|
|
31
|
+
existing_filters[filter_name]["pipeline"].extend(config.get("pipeline", []))
|
|
32
|
+
# do not override fail_silently
|
|
33
|
+
if "fail_silently" in config:
|
|
34
|
+
existing_filters[filter_name].setdefault(
|
|
35
|
+
"fail_silently", config["fail_silently"]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
settings.OPEN_EDX_FILTERS_CONFIG = existing_filters
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URL configuration for ol_openedx_auto_select_language app.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.urls import re_path
|
|
7
|
+
|
|
8
|
+
from ol_openedx_auto_select_language.views import CourseLanguageView
|
|
9
|
+
|
|
10
|
+
urlpatterns = [
|
|
11
|
+
re_path(
|
|
12
|
+
rf"auto-select-language/api/course-language/{settings.COURSE_KEY_PATTERN}$",
|
|
13
|
+
CourseLanguageView.as_view(),
|
|
14
|
+
name="ol_course_language",
|
|
15
|
+
),
|
|
16
|
+
# TODO: Remove the legacy endpoint in a # noqa: FIX002, TD003, TD002
|
|
17
|
+
# future release after updating all clients to use the new endpoint.
|
|
18
|
+
re_path(
|
|
19
|
+
rf"course-translations/api/course-language/{settings.COURSE_KEY_PATTERN}$",
|
|
20
|
+
CourseLanguageView.as_view(),
|
|
21
|
+
name="ol_course_language_legacy",
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utils for ol_openedx_auto_select_language.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LanguageCode:
|
|
9
|
+
"""
|
|
10
|
+
Utility class for handling language code conversions between
|
|
11
|
+
Django/Open edX style and BCP47.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, lang_code):
|
|
15
|
+
self.lang_code = lang_code
|
|
16
|
+
|
|
17
|
+
def to_bcp47(self) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Convert Django / Open edX style language codes to BCP47.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
zh_HANS -> zh-Hans
|
|
23
|
+
zh_HANT -> zh-Hant
|
|
24
|
+
zh_HANS_CN -> zh-Hans-CN
|
|
25
|
+
en_US -> en-US
|
|
26
|
+
es_419 -> es-419
|
|
27
|
+
pt_br -> pt-BR
|
|
28
|
+
"""
|
|
29
|
+
if not self.lang_code:
|
|
30
|
+
return self.lang_code
|
|
31
|
+
|
|
32
|
+
parts = self.lang_code.replace("_", "-").split("-")
|
|
33
|
+
result = []
|
|
34
|
+
for idx, part in enumerate(parts):
|
|
35
|
+
if idx == 0:
|
|
36
|
+
# Language
|
|
37
|
+
result.append(part.lower())
|
|
38
|
+
|
|
39
|
+
elif re.fullmatch(r"[A-Za-z]{4}", part):
|
|
40
|
+
# Script (Hans, Hant, Latn, Cyrl, etc.)
|
|
41
|
+
result.append(part.title())
|
|
42
|
+
|
|
43
|
+
elif re.fullmatch(r"[A-Za-z]{2}", part):
|
|
44
|
+
# Region i.e US, PK, CN
|
|
45
|
+
result.append(part.upper())
|
|
46
|
+
|
|
47
|
+
elif re.fullmatch(r"\d{3}", part):
|
|
48
|
+
# Numeric region (419)
|
|
49
|
+
result.append(part)
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
# Variants/extensions
|
|
53
|
+
result.append(part.lower())
|
|
54
|
+
|
|
55
|
+
return "-".join(result)
|
|
56
|
+
|
|
57
|
+
def to_django(self) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Convert BCP47 language tags to Django / Open edX style.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
zh-Hans -> zh_HANS
|
|
63
|
+
zh-Hant -> zh_HANT
|
|
64
|
+
zh-Hans-CN -> zh_HANS_CN
|
|
65
|
+
en-US -> en_US
|
|
66
|
+
es-419 -> es_419
|
|
67
|
+
pt-BR -> pt_BR
|
|
68
|
+
"""
|
|
69
|
+
if not self.lang_code:
|
|
70
|
+
return self.lang_code
|
|
71
|
+
|
|
72
|
+
parts = self.lang_code.replace("_", "-").split("-")
|
|
73
|
+
result = []
|
|
74
|
+
|
|
75
|
+
for idx, part in enumerate(parts):
|
|
76
|
+
if idx == 0:
|
|
77
|
+
# Language
|
|
78
|
+
result.append(part.lower())
|
|
79
|
+
|
|
80
|
+
elif re.fullmatch(r"[A-Za-z]{4}", part):
|
|
81
|
+
# Script
|
|
82
|
+
result.append(part.upper())
|
|
83
|
+
|
|
84
|
+
elif re.fullmatch(r"[A-Za-z]{2}", part):
|
|
85
|
+
# Region
|
|
86
|
+
result.append(part.upper())
|
|
87
|
+
|
|
88
|
+
elif re.fullmatch(r"\d{3}", part):
|
|
89
|
+
# Numeric region
|
|
90
|
+
result.append(part)
|
|
91
|
+
|
|
92
|
+
else:
|
|
93
|
+
# Variants/extensions
|
|
94
|
+
result.append(part.lower())
|
|
95
|
+
|
|
96
|
+
return "_".join(result)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Views for ol_openedx_auto_select_language App
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from opaque_keys import InvalidKeyError
|
|
8
|
+
from opaque_keys.edx.keys import CourseKey
|
|
9
|
+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
10
|
+
from rest_framework import status
|
|
11
|
+
from rest_framework.permissions import IsAuthenticated
|
|
12
|
+
from rest_framework.response import Response
|
|
13
|
+
from rest_framework.views import APIView
|
|
14
|
+
|
|
15
|
+
from ol_openedx_auto_select_language.utils import LanguageCode
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CourseLanguageView(APIView):
|
|
21
|
+
"""
|
|
22
|
+
API View to retrieve the language of a specified course.
|
|
23
|
+
|
|
24
|
+
Sample Request:
|
|
25
|
+
GET /auto-select-language/api/course_language/{course_key}/
|
|
26
|
+
|
|
27
|
+
Sample Response:
|
|
28
|
+
200 OK
|
|
29
|
+
{
|
|
30
|
+
"language": "en"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Error Responses:
|
|
34
|
+
400 Bad Request
|
|
35
|
+
{
|
|
36
|
+
"error": "Invalid course_key."
|
|
37
|
+
}
|
|
38
|
+
404 Not Found
|
|
39
|
+
{
|
|
40
|
+
"error": "Course not found."
|
|
41
|
+
}
|
|
42
|
+
400 Bad Request
|
|
43
|
+
{
|
|
44
|
+
"error": "An unexpected error occurred."
|
|
45
|
+
}
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
permission_classes = [IsAuthenticated]
|
|
49
|
+
|
|
50
|
+
def get(self, request, course_key_string): # noqa: ARG002
|
|
51
|
+
"""
|
|
52
|
+
Retrieve the language of the specified course.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
course_key = CourseKey.from_string(course_key_string)
|
|
56
|
+
course = CourseOverview.get_from_id(course_key)
|
|
57
|
+
except InvalidKeyError:
|
|
58
|
+
log.info("Invalid course key %s", course_key_string)
|
|
59
|
+
return Response(
|
|
60
|
+
{"error": "Invalid course_key."},
|
|
61
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
62
|
+
)
|
|
63
|
+
except CourseOverview.DoesNotExist:
|
|
64
|
+
log.info("Course not found for key %s", course_key_string)
|
|
65
|
+
return Response(
|
|
66
|
+
{"error": "Course not found."},
|
|
67
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
68
|
+
)
|
|
69
|
+
except Exception:
|
|
70
|
+
log.exception("Unexpected error retrieving course %s", course_key_string)
|
|
71
|
+
return Response(
|
|
72
|
+
{"error": "An unexpected error occurred."},
|
|
73
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
74
|
+
)
|
|
75
|
+
language = LanguageCode(course.language).to_bcp47()
|
|
76
|
+
return Response({"language": language})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ol-openedx-auto-select-language"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "An Open edX plugin to auto-select the language based on course language settings."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "MIT Office of Digital Learning"}
|
|
7
|
+
]
|
|
8
|
+
license = "BSD-3-Clause"
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
keywords = ["Python", "edx"]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"Django>=4.0",
|
|
14
|
+
"djangorestframework>=3.14.0",
|
|
15
|
+
"edx-opaque-keys",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.entry-points."cms.djangoapp"]
|
|
19
|
+
ol_openedx_auto_select_language = "ol_openedx_auto_select_language.apps:OLOpenEdxAutoSelectLanguageConfig"
|
|
20
|
+
|
|
21
|
+
[project.entry-points."lms.djangoapp"]
|
|
22
|
+
ol_openedx_auto_select_language = "ol_openedx_auto_select_language.apps:OLOpenEdxAutoSelectLanguageConfig"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["ol_openedx_auto_select_language"]
|
|
30
|
+
include = [
|
|
31
|
+
"ol_openedx_auto_select_language/**/*.py",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = [
|
|
36
|
+
"ol_openedx_auto_select_language/**/*",
|
|
37
|
+
"README.rst",
|
|
38
|
+
"pyproject.toml",
|
|
39
|
+
]
|