mirror of
https://github.com/ChristianLight/tutor.git
synced 2024-06-06 16:10:48 +00:00
Merge remote-tracking branch 'origin/master' into nightly
This commit is contained in:
commit
2bfd33820a
|
@ -4,6 +4,11 @@ Note: Breaking changes between versions are indicated by "💥".
|
|||
|
||||
## Unreleased
|
||||
|
||||
## v13.1.1 (2022-01-25)
|
||||
|
||||
- [Bugfix] Fix authentication in development due to missing SameSite policy on session ID cookie.
|
||||
- [Bugfix] Display properly themed favicon.ico image in LMS, Studio and microfrontends.
|
||||
- [Bugfix] Fix "LazyStaticAbsoluteUrl is not JSON serializable" error when sending bulk emails.
|
||||
- [Bugfix] Fix `tutor local importdemocourse` fails when platform is not up.
|
||||
|
||||
## v13.1.0 (2022-01-08)
|
||||
|
|
|
@ -154,6 +154,11 @@ First of all, stop any locally-running platform::
|
|||
tutor local stop
|
||||
tutor dev stop
|
||||
|
||||
Remove all containers::
|
||||
|
||||
tutor local dc down --remove-orphans
|
||||
tutor local dc down --remove-orphans
|
||||
|
||||
Then, delete all data associated to your Open edX platform::
|
||||
|
||||
# WARNING: this step is irreversible
|
||||
|
|
|
@ -8,7 +8,7 @@ from tutor.types import Config, get_typed
|
|||
|
||||
class PluginsTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
plugins.Plugins.clear()
|
||||
plugins.Plugins.clear_cache()
|
||||
|
||||
@patch.object(plugins.DictPlugin, "iter_installed", return_value=[])
|
||||
def test_iter_installed(self, dict_plugin_iter_installed: Mock) -> None:
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
# Increment this version number to trigger a new release. See
|
||||
# docs/tutor.html#versioning for information on the versioning scheme.
|
||||
__version__ = "13.1.0"
|
||||
__version__ = "13.1.1"
|
||||
|
||||
# The version suffix will be appended to the actual version, separated by a
|
||||
# dash. Use this suffix to differentiate between the actual released version and
|
||||
|
|
|
@ -74,29 +74,21 @@ class BasePlugin:
|
|||
config = get_callable_attr(obj, "config", {})
|
||||
if not isinstance(config, dict):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid config in plugin {}. Expected dict, got {}.".format(
|
||||
plugin_name, config.__class__
|
||||
)
|
||||
f"Invalid config in plugin {plugin_name}. Expected dict, got {config.__class__}."
|
||||
)
|
||||
for name, subconfig in config.items():
|
||||
if not isinstance(name, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid config entry '{}' in plugin {}. Expected str, got {}.".format(
|
||||
name, plugin_name, config.__class__
|
||||
)
|
||||
f"Invalid config entry '{name}' in plugin {plugin_name}. Expected str, got {config.__class__}."
|
||||
)
|
||||
if not isinstance(subconfig, dict):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid config entry '{}' in plugin {}. Expected str keys, got {}.".format(
|
||||
name, plugin_name, config.__class__
|
||||
)
|
||||
f"Invalid config entry '{name}' in plugin {plugin_name}. Expected str keys, got {config.__class__}."
|
||||
)
|
||||
for key in subconfig.keys():
|
||||
if not isinstance(key, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid config entry '{}.{}' in plugin {}. Expected str, got {}.".format(
|
||||
name, key, plugin_name, key.__class__
|
||||
)
|
||||
f"Invalid config entry '{name}.{key}' in plugin {plugin_name}. Expected str, got {key.__class__}."
|
||||
)
|
||||
return config
|
||||
|
||||
|
@ -108,22 +100,16 @@ class BasePlugin:
|
|||
patches = get_callable_attr(obj, "patches", {})
|
||||
if not isinstance(patches, dict):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid patches in plugin {}. Expected dict, got {}.".format(
|
||||
plugin_name, patches.__class__
|
||||
)
|
||||
f"Invalid patches in plugin {plugin_name}. Expected dict, got {patches.__class__}."
|
||||
)
|
||||
for patch_name, content in patches.items():
|
||||
if not isinstance(patch_name, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid patch name '{}' in plugin {}. Expected str, got {}.".format(
|
||||
patch_name, plugin_name, patch_name.__class__
|
||||
)
|
||||
f"Invalid patch name '{patch_name}' in plugin {plugin_name}. Expected str, got {patch_name.__class__}."
|
||||
)
|
||||
if not isinstance(content, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid patch '{}' in plugin {}. Expected str, got {}.".format(
|
||||
patch_name, plugin_name, content.__class__
|
||||
)
|
||||
f"Invalid patch '{patch_name}' in plugin {plugin_name}. Expected str, got {content.__class__}."
|
||||
)
|
||||
return patches
|
||||
|
||||
|
@ -137,38 +123,28 @@ class BasePlugin:
|
|||
hooks = get_callable_attr(obj, "hooks", default={})
|
||||
if not isinstance(hooks, dict):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid hooks in plugin {}. Expected dict, got {}.".format(
|
||||
plugin_name, hooks.__class__
|
||||
)
|
||||
f"Invalid hooks in plugin {plugin_name}. Expected dict, got {hooks.__class__}."
|
||||
)
|
||||
for hook_name, hook in hooks.items():
|
||||
if not isinstance(hook_name, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid hook name '{}' in plugin {}. Expected str, got {}.".format(
|
||||
hook_name, plugin_name, hook_name.__class__
|
||||
)
|
||||
f"Invalid hook name '{hook_name}' in plugin {plugin_name}. Expected str, got {hook_name.__class__}."
|
||||
)
|
||||
if isinstance(hook, list):
|
||||
for service in hook:
|
||||
if not isinstance(service, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid service in hook '{}' from plugin {}. Expected str, got {}.".format(
|
||||
hook_name, plugin_name, service.__class__
|
||||
)
|
||||
f"Invalid service in hook '{hook_name}' from plugin {plugin_name}. Expected str, got {service.__class__}."
|
||||
)
|
||||
elif isinstance(hook, dict):
|
||||
for name, value in hook.items():
|
||||
if not isinstance(name, str) or not isinstance(value, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid hook '{}' in plugin {}. Only str -> str entries are supported.".format(
|
||||
hook_name, plugin_name
|
||||
)
|
||||
f"Invalid hook '{hook_name}' in plugin {plugin_name}. Only str -> str entries are supported."
|
||||
)
|
||||
else:
|
||||
raise exceptions.TutorError(
|
||||
"Invalid hook '{}' in plugin {}. Expected dict or list, got {}.".format(
|
||||
hook_name, plugin_name, hook.__class__
|
||||
)
|
||||
f"Invalid hook '{hook_name}' in plugin {plugin_name}. Expected dict or list, got {hook.__class__}."
|
||||
)
|
||||
return hooks
|
||||
|
||||
|
@ -206,6 +182,11 @@ class BasePlugin:
|
|||
def iter_load(cls) -> Iterator["BasePlugin"]:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls) -> None:
|
||||
cls._IS_LOADED = False
|
||||
cls.INSTALLED.clear()
|
||||
|
||||
|
||||
class EntrypointPlugin(BasePlugin):
|
||||
"""
|
||||
|
@ -235,13 +216,11 @@ class EntrypointPlugin(BasePlugin):
|
|||
yield cls(entrypoint)
|
||||
except pkg_resources.VersionConflict as e:
|
||||
error = e.report()
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
error = str(e)
|
||||
if error:
|
||||
fmt.echo_error(
|
||||
"Failed to load entrypoint '{} = {}' from distribution {}: {}".format(
|
||||
entrypoint.name, entrypoint.module_name, entrypoint.dist, error
|
||||
)
|
||||
f"Failed to load entrypoint '{entrypoint.name} = {entrypoint.module_name}' from distribution {entrypoint.dist}: {error}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -258,7 +237,7 @@ class OfficialPlugin(BasePlugin):
|
|||
return plugin
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.module = importlib.import_module("tutor{}.plugin".format(name))
|
||||
self.module = importlib.import_module(f"tutor{name}.plugin")
|
||||
super().__init__(name, self.module)
|
||||
|
||||
@property
|
||||
|
@ -283,9 +262,7 @@ class DictPlugin(BasePlugin):
|
|||
name = data["name"]
|
||||
if not isinstance(name, str):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid plugin name: '{}'. Expected str, got {}".format(
|
||||
name, name.__class__
|
||||
)
|
||||
f"Invalid plugin name: '{name}'. Expected str, got {name.__class__}"
|
||||
)
|
||||
|
||||
# Create a generic object (sort of a named tuple) which will contain all key/values from data
|
||||
|
@ -310,17 +287,17 @@ class DictPlugin(BasePlugin):
|
|||
@classmethod
|
||||
def iter_load(cls) -> Iterator[BasePlugin]:
|
||||
for path in glob(os.path.join(cls.ROOT, "*.yml")):
|
||||
with open(path) as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = serialize.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise exceptions.TutorError(
|
||||
"Invalid plugin: {}. Expected dict.".format(path)
|
||||
f"Invalid plugin: {path}. Expected dict."
|
||||
)
|
||||
try:
|
||||
yield cls(data)
|
||||
except KeyError as e:
|
||||
raise exceptions.TutorError(
|
||||
"Invalid plugin: {}. Missing key: {}".format(path, e.args[0])
|
||||
f"Invalid plugin: {path}. Missing key: {e.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -352,9 +329,9 @@ class Plugins:
|
|||
self.hooks[hook_name][plugin.name] = services
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
def clear_cache(cls) -> None:
|
||||
for PluginClass in cls.PLUGIN_CLASSES:
|
||||
PluginClass.INSTALLED.clear()
|
||||
PluginClass.clear_cache()
|
||||
|
||||
@classmethod
|
||||
def iter_installed(cls) -> Iterator[BasePlugin]:
|
||||
|
@ -411,7 +388,7 @@ def iter_installed() -> Iterator[BasePlugin]:
|
|||
|
||||
def enable(config: Config, name: str) -> None:
|
||||
if not is_installed(name):
|
||||
raise exceptions.TutorError("plugin '{}' is not installed.".format(name))
|
||||
raise exceptions.TutorError(f"plugin '{name}' is not installed.")
|
||||
if is_enabled(config, name):
|
||||
return
|
||||
enabled = enabled_plugins(config)
|
||||
|
@ -434,7 +411,7 @@ def get_enabled(config: Config, name: str) -> BasePlugin:
|
|||
for plugin in iter_enabled(config):
|
||||
if plugin.name == name:
|
||||
return plugin
|
||||
raise ValueError("Enabled plugin {} could not be found.".format(plugin.name))
|
||||
raise ValueError(f"Enabled plugin {name} could not be found.")
|
||||
|
||||
|
||||
def iter_enabled(config: Config) -> Iterator[BasePlugin]:
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
|
||||
{{ LMS_HOST }}{$default_site_port}, {{ PREVIEW_LMS_HOST }}{$default_site_port} {
|
||||
@favicon_matcher {
|
||||
path_regexp ^(.*)/favicon.ico$
|
||||
path_regexp ^/favicon.ico$
|
||||
}
|
||||
rewrite @favicon_matcher /static/images/favicon.ico
|
||||
rewrite @favicon_matcher /theming/asset/images/favicon.ico
|
||||
|
||||
# Limit profile image upload size
|
||||
request_body /api/profile_images/*/*/upload {
|
||||
|
@ -46,9 +46,9 @@
|
|||
|
||||
{{ CMS_HOST }}{$default_site_port} {
|
||||
@favicon_matcher {
|
||||
path_regexp ^(.*)/favicon.ico$
|
||||
path_regexp ^/favicon.ico$
|
||||
}
|
||||
rewrite @favicon_matcher /static/images/favicon.ico
|
||||
rewrite @favicon_matcher /theming/asset/images/favicon.ico
|
||||
|
||||
request_body {
|
||||
max_size 250MB
|
||||
|
|
|
@ -7,8 +7,6 @@ from lms.envs.devstack import *
|
|||
# Setup correct webpack configuration file for development
|
||||
WEBPACK_CONFIG_PATH = "webpack.dev.config.js"
|
||||
|
||||
SESSION_COOKIE_DOMAIN = ".{{ LMS_HOST|common_domain(CMS_HOST) }}"
|
||||
|
||||
LMS_BASE = "{{ LMS_HOST}}:8000"
|
||||
LMS_ROOT_URL = "http://{}".format(LMS_BASE)
|
||||
LMS_INTERNAL_ROOT_URL = LMS_ROOT_URL
|
||||
|
@ -17,6 +15,12 @@ CMS_BASE = "{{ CMS_HOST}}:8001"
|
|||
CMS_ROOT_URL = "http://{}".format(CMS_BASE)
|
||||
LOGIN_REDIRECT_WHITELIST.append(CMS_BASE)
|
||||
|
||||
# Session cookie
|
||||
SESSION_COOKIE_DOMAIN = "{{ LMS_HOST }}"
|
||||
SESSION_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
|
||||
# CMS authentication
|
||||
IDA_LOGOUT_URI_LIST.append("http://{{ CMS_HOST }}:8001/complete/logout")
|
||||
|
||||
|
|
|
@ -19,25 +19,7 @@ COURSE_ABOUT_VISIBILITY_PERMISSION = "see_about_page"
|
|||
OAUTH_ENFORCE_SECURE = False
|
||||
|
||||
# Email settings
|
||||
class LazyStaticAbsoluteUrl:
|
||||
"""
|
||||
Evaluates a static asset path lazily at runtime
|
||||
"""
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def __str__(self):
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
return settings.LMS_ROOT_URL + staticfiles_storage.url(self.path)
|
||||
|
||||
def to_json(self):
|
||||
# This method is required for json serialization by edx-ace, notably for
|
||||
# serialization of the registration email. See
|
||||
# edx_ace.serialization.MessageEncoder.
|
||||
return str(self)
|
||||
# We need a lazily-computed logo url to capture the url of the theme-specific logo.
|
||||
DEFAULT_EMAIL_LOGO_URL = LazyStaticAbsoluteUrl("images/logo.png")
|
||||
DEFAULT_EMAIL_LOGO_URL = LMS_ROOT_URL + "/theming/asset/images/logo.png"
|
||||
|
||||
# Create folders if necessary
|
||||
for folder in [DATA_DIR, LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE, ORA2_FILEUPLOAD_ROOT]:
|
||||
|
|
Loading…
Reference in New Issue
Block a user