-
Notifications
You must be signed in to change notification settings - Fork 55
Draft: add rdmo.config app for Plugin model (plugin management)
#1436
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
base: 2.5.0/release
Are you sure you want to change the base?
Changes from all commits
f90b6e3
fc11cd2
061ce24
7e07fd1
f764449
bf829e7
0eba427
921c9ec
2856b20
8d29e6f
fdb4ca7
2bf6779
4ebc212
38cd0a9
864e0fa
52a9975
7309a45
5d464bf
8e740d7
1ba1ad1
147ea53
1617950
0702045
dd365ba
ba1bfea
e75704f
90f6b7a
41feaa0
07ee628
f67c629
50140f6
fbf7a38
6859dfd
8935833
c44ad89
9c9f73d
08b9527
7f05adb
57c5e90
b47bff9
d4e2d1b
64c18f5
287feed
2b3e81b
ccaab58
460c62c
bbe0fcf
545a4cf
88a6b0d
c6693c5
fa930db
7990ba5
a38a302
9f9dff4
3469914
ce9c820
627df20
29daaf5
b585b86
14de0fd
aaf9609
317e07b
394114d
d0b01e2
5e47f1e
2d4a56a
0491e35
f07c4d6
5f5fe9c
a6cead2
cecf9a3
8e79f79
4bbad43
6ec2d43
d2ca0dc
707f566
6754001
a523400
0d7c42b
2a77e49
8aabf3f
32d408c
42a54e6
d3f2663
07f8414
106d178
b6260c9
a68adca
03faa2d
bc1d19f
1f2ecfd
6f3a3bd
0ffb48a
35b3bdd
8f66bac
9108f28
0ffdc3b
d4e590a
ed97b32
3445ee2
b176e23
92ee088
7b7b2f5
06a503e
5c1ae6e
9ecf292
ce6e187
3f7b5aa
4d46f64
366c7ad
1b178d1
af00d09
1baa197
4e23781
90dccfb
6d6cc5d
afb44a9
a8a6512
59c77f4
0467e35
261b84e
d97fc91
fc80933
04f5188
7d2501f
9e5445a
9e42d48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| from django import forms | ||
| from django.contrib import admin | ||
|
|
||
| from rdmo.core.admin import ElementAdminForm | ||
| from rdmo.core.utils import get_language_fields, get_plugin_python_paths | ||
|
|
||
| from .models import Plugin | ||
| from .validators import ( | ||
| PluginLockedValidator, | ||
| PluginPythonPathValidator, | ||
| PluginUniqueURIValidator, | ||
| PluginURLNameValidator, | ||
| ) | ||
|
|
||
|
|
||
| class PluginAdminForm(ElementAdminForm): | ||
|
|
||
| python_path = forms.ChoiceField(choices=[(plugin, plugin) for plugin in get_plugin_python_paths()]) | ||
|
|
||
|
|
||
| class Meta: | ||
| model = Plugin | ||
| fields = '__all__' | ||
|
|
||
| def clean(self): | ||
| PluginUniqueURIValidator(self.instance)(self.cleaned_data) | ||
| PluginLockedValidator(self.instance)(self.cleaned_data) | ||
| PluginPythonPathValidator(self.instance)(self.cleaned_data) | ||
| PluginURLNameValidator(self.instance)(self.cleaned_data) | ||
|
|
||
|
|
||
| @admin.register(Plugin) | ||
| class PluginAdmin(admin.ModelAdmin): | ||
| form = PluginAdminForm | ||
|
|
||
| search_fields = ['uri', 'python_path', *get_language_fields('title'), *get_language_fields('help')] | ||
| list_display = ('uri', 'python_path', 'plugin_type', 'available') | ||
| readonly_fields = ('uri', 'plugin_type') | ||
| list_filter = ('available', 'python_path', 'sites' , 'groups', 'catalogs') | ||
| filter_horizontal = ('catalogs', 'sites', 'editors', 'groups') | ||
| ordering = ('python_path','order') | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from django.apps import AppConfig | ||
| from django.utils.translation import gettext_lazy as _ | ||
|
|
||
|
|
||
| class ConfigConfig(AppConfig): | ||
| name = 'rdmo.config' | ||
| verbose_name = _('Config') | ||
|
|
||
| def ready(self): | ||
| from . import checks # noqa: F401 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| from collections import defaultdict | ||
|
|
||
| from django.core.checks import Warning, register | ||
| from django.utils.module_loading import import_string | ||
|
|
||
|
|
||
| @register() | ||
| def deprecated_plugin_settings_check(app_configs, **kwargs): | ||
| from django.conf import settings | ||
| legacy_settings = [ | ||
| "PROJECT_EXPORTS", | ||
| "PROJECT_SNAPSHOT_EXPORTS", | ||
| "PROJECT_IMPORTS", | ||
| "PROJECT_ISSUE_PROVIDERS", | ||
| "PROJECT_IMPORTS_LIST", | ||
| "OPTIONSET_PROVIDERS", | ||
| ] | ||
| issues = [] | ||
| legacy_settings_used_keys = { | ||
| i for i in legacy_settings if hasattr(settings, i) | ||
| } | ||
| if legacy_settings_used_keys: | ||
| _verb = "are" if len(legacy_settings_used_keys) > 1 else "is" | ||
| legacy_settings = { | ||
| i: getattr(settings, i) for i in legacy_settings_used_keys | ||
| } | ||
| _legacy_settings_plugins = defaultdict(list) | ||
| _python_paths = set() | ||
| for name,entries in legacy_settings.items(): | ||
| for entry in entries: | ||
| _legacy_settings_plugins[name].append(entry) | ||
| if len(entry) == 3: | ||
| _python_paths.add(entry[-1]) | ||
| issues.append(Warning( | ||
| f"{', '.join(legacy_settings_used_keys)} {_verb} deprecated as of RDMO 2.5.0; " | ||
| f"use PLUGINS = ['python.dotted.paths', ...] instead.", | ||
| id="rdmo.config.W001", | ||
| hint="Define the legacy plugin settings in PLUGINS and remove the legacy settings." | ||
| f"\n{repr_new_settings(_python_paths)}", | ||
| )) | ||
| # If both PLUGINS and any legacy key exist | ||
| if settings.PLUGINS: | ||
| issues.append(Warning( | ||
| "PLUGINS is set in addition to legacy settings; the legacy settings are ignored.", | ||
| id="rdmo.config.W002", | ||
| hint="Remove the following legacy settings to avoid confusion: " | ||
| f"{', '.join(legacy_settings_used_keys)}." | ||
| )) | ||
| return issues | ||
|
|
||
|
MyPyDavid marked this conversation as resolved.
|
||
|
|
||
| def repr_new_settings(python_paths) -> str: | ||
| if not python_paths: | ||
| return "" | ||
| elif len(python_paths) == 1: | ||
| return f"PLUGINS = ['{python_paths}']" | ||
| msg = "PLUGINS = [" | ||
| for python_path in sorted(python_paths): | ||
| msg += f"\n\t{python_path}, " | ||
| msg += "\n]" | ||
| return msg | ||
|
|
||
|
|
||
| @register() | ||
| def plugins_importable_check(app_configs, **kwargs): | ||
| from django.conf import settings | ||
|
|
||
| issues = [] | ||
| for plugin_path in settings.PLUGINS: | ||
| try: | ||
| import_string(plugin_path) | ||
| except ImportError as exc: | ||
| issues.append(Warning( | ||
| f"Plugin import failed: {plugin_path} ({exc})", | ||
| id="rdmo.config.W003", | ||
| hint="Ensure the plugin path is valid and the module is installed.", | ||
| )) | ||
| return issues | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from django.db.models import TextChoices | ||
|
|
||
|
|
||
| class PLUGIN_TYPES(TextChoices): | ||
| PROJECT_EXPORT = "project_export", "Project export" | ||
| PROJECT_SNAPSHOT_EXPORT = "project_snapshot_export", "Project snapshot export" | ||
| PROJECT_IMPORT = "project_import", "Project import" | ||
| PROJECT_ISSUE_PROVIDER = "project_issue_provider", "Project issue provider" | ||
| OPTIONSET_PROVIDER = "optionset_provider", "Optionset provider" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper | ||
|
|
||
| from .models import Plugin | ||
| from .validators import PluginLockedValidator, PluginUniqueURIValidator | ||
|
|
||
| import_helper_plugin = ElementImportHelper( | ||
| model=Plugin, | ||
| validators=(PluginLockedValidator, PluginUniqueURIValidator), | ||
| lang_fields=('title', 'help'), | ||
| extra_fields=( | ||
| ExtraFieldHelper(field_name='python_path'), | ||
| ExtraFieldHelper(field_name='plugin_settings', overwrite_in_element=True), | ||
| ExtraFieldHelper(field_name='available', overwrite_in_element=True), | ||
| ExtraFieldHelper(field_name='locked'), | ||
| ExtraFieldHelper(field_name='order'), | ||
| ExtraFieldHelper(field_name='url_name'), | ||
|
|
||
| ), | ||
| add_current_site_sites = True, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from django.conf import settings | ||
|
|
||
| from rdmo.config.constants import PLUGIN_TYPES | ||
|
|
||
| PLUGIN_TYPE_TO_SETTING_KEY = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is redundant. |
||
| PLUGIN_TYPES.PROJECT_IMPORT: "PROJECT_IMPORTS", | ||
| PLUGIN_TYPES.PROJECT_EXPORT: "PROJECT_EXPORTS", | ||
| PLUGIN_TYPES.PROJECT_SNAPSHOT_EXPORT: "PROJECT_SNAPSHOT_EXPORTS", | ||
| PLUGIN_TYPES.PROJECT_ISSUE_PROVIDER: "PROJECT_ISSUE_PROVIDERS", | ||
| PLUGIN_TYPES.OPTIONSET_PROVIDER: "OPTIONSET_PROVIDERS", | ||
| } | ||
|
|
||
| def get_plugins_from_legacy_settings(select_plugin_type=None) -> list[dict]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| """Read 3-tuples (key, label, python-path) from legacy settings.""" | ||
| plugin_definitions: list[dict] = [] | ||
| for plugin_type, legacy_setting in PLUGIN_TYPE_TO_SETTING_KEY.items(): | ||
| if not hasattr(settings, legacy_setting): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary. |
||
| continue | ||
| if select_plugin_type is not None and select_plugin_type != plugin_type: | ||
| continue | ||
|
|
||
| legacy_plugins = getattr(settings, legacy_setting, None) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if not legacy_plugins: | ||
| continue | ||
|
|
||
| for entry in legacy_plugins: | ||
| try: | ||
| key, label, dotted = entry | ||
| except ValueError as exc: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is not needed. |
||
| raise ValueError( | ||
| f"{legacy_setting} must be a sequence of 3-tuples " | ||
| f"(key, label, python-path); got {entry!r}" | ||
| ) from exc | ||
|
|
||
| plugin_definitions.append({ | ||
| "uri_prefix": settings.DEFAULT_URI_PREFIX, | ||
| "uri_path": f"{legacy_setting.lower()}/{key}", | ||
| "title": label, | ||
| "python_path": dotted, | ||
| "plugin_type": plugin_type, | ||
| "url_name": key, | ||
| }) | ||
|
|
||
| return plugin_definitions | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from django.core.management.base import BaseCommand | ||
| from django.utils.module_loading import import_string | ||
|
|
||
| from rdmo.config.models import Plugin | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = "Check that all configured plugins can be imported" | ||
|
|
||
| def handle(self, *args, **options): | ||
| if not Plugin.objects.exists(): | ||
| self.stdout.write(self.style.SUCCESS("No plugins found.")) | ||
|
|
||
| for plugin in Plugin.objects.order_by('python_path').all(): | ||
| try: | ||
| import_string(plugin.python_path) | ||
| self.stdout.write(self.style.SUCCESS(f"✔ {plugin.python_path}, type={plugin.plugin_type}.")) | ||
| except ImportError as e: | ||
| if plugin.available: | ||
| self.stdout.write(self.style.ERROR( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The url of the plugin should be displayed here. |
||
| f"✖ {plugin.python_path}, type={plugin.plugin_type} failed: {e}") | ||
| ) | ||
| else: | ||
| self.stdout.write(self.style.WARNING( | ||
| f"!! {plugin.python_path}, type={plugin.plugin_type} (=unavailable) failed: {e}") | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.