From 0f20403f45e13c60cdd57042814d707a73deee8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Sat, 11 Nov 2023 00:58:10 +0100 Subject: [PATCH] feat(plugins): validator for cross-plugin configuration validation --- .../sections/forms/plugin_configuration.py | 16 +++- fiesta/apps/sections/forms/plugin_state.py | 12 +++ .../services/sections_plugins_validator.py | 75 +++++++++++++++++++ .../sections/parts/plugin_state_form.html | 3 +- fiesta/apps/sections/views/plugins.py | 22 +++++- 5 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 fiesta/apps/sections/services/sections_plugins_validator.py diff --git a/fiesta/apps/sections/forms/plugin_configuration.py b/fiesta/apps/sections/forms/plugin_configuration.py index 0e4c9945..50183865 100644 --- a/fiesta/apps/sections/forms/plugin_configuration.py +++ b/fiesta/apps/sections/forms/plugin_configuration.py @@ -1,15 +1,29 @@ from __future__ import annotations +from django.core.exceptions import ValidationError from django.forms import modelform_factory from apps.fiestaforms.forms import BaseModelForm from apps.plugins.models import BasePluginConfiguration +from apps.sections.models import Section +from apps.sections.services.sections_plugins_validator import SectionsPluginsValidator -def get_plugin_configuration_form(configuration: BasePluginConfiguration) -> type[BaseModelForm]: +def get_plugin_configuration_form(configuration: BasePluginConfiguration, for_section: Section) -> type[BaseModelForm]: class BaseConfigurationForm(BaseModelForm): template_name = "sections/parts/plugin_configuration_form.html" + def _post_clean(self): + super()._post_clean() + + try: + SectionsPluginsValidator.for_changed_conf( + section=for_section, + conf=self.instance, + ).check_validity() + except ValidationError as e: + self.add_error(None, e) + return modelform_factory( configuration.__class__, form=BaseConfigurationForm, diff --git a/fiesta/apps/sections/forms/plugin_state.py b/fiesta/apps/sections/forms/plugin_state.py index a4a73e3c..2b825c27 100644 --- a/fiesta/apps/sections/forms/plugin_state.py +++ b/fiesta/apps/sections/forms/plugin_state.py @@ -9,6 +9,7 @@ from apps.plugins.models import Plugin from apps.plugins.plugin import BasePluginAppConfig from apps.plugins.utils import all_plugins_mapped_to_label +from apps.sections.services.sections_plugins_validator import SectionsPluginsValidator class ChangePluginStateForm(BaseModelForm): @@ -30,6 +31,17 @@ def clean(self): return data + def _post_clean(self): + super()._post_clean() + + try: + SectionsPluginsValidator.for_changed_plugin( + section=self.instance.section, + plugin=self.instance, + ).check_validity() + except ValidationError as e: + self.add_error(None, e) + class SetupPluginSettingsForm(ChangePluginStateForm): instance: Plugin diff --git a/fiesta/apps/sections/services/sections_plugins_validator.py b/fiesta/apps/sections/services/sections_plugins_validator.py new file mode 100644 index 00000000..3b2340ab --- /dev/null +++ b/fiesta/apps/sections/services/sections_plugins_validator.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import dataclasses + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from apps.buddy_system.apps import BuddySystemConfig +from apps.plugins.models import BasePluginConfiguration, Plugin +from apps.plugins.plugin import BasePluginAppConfig +from apps.plugins.utils import all_plugins_mapped_to_class +from apps.sections.apps import SectionsConfig +from apps.sections.models import Section, SectionsConfiguration + + +@dataclasses.dataclass(frozen=True) +class SectionsPluginsValidator: + """Defines relations between plugin configurations and validates them.""" + + section: Section + + plugins: dict[str, Plugin] + configurations: dict[str, BasePluginConfiguration] + + def check_validity(self): + """Checks if all plugin configurations are valid.""" + for p in self.plugins.values(): + self._check_for_plugin(p) + + def _check_for_plugin(self, plugin: Plugin): + match plugin.app_config: + case BuddySystemConfig() | SectionsConfig(): + if not self.has_enabled_plugin(BuddySystemConfig): + return + + sections_conf: SectionsConfiguration = self.get_configuration(SectionsConfig) + + if not sections_conf.required_faculty: + raise ValidationError( + _( + "With enabled Buddy system plugin, you need to require faculty " + "in Section plugin configuration." + ) + ) + + def has_enabled_plugin(self, app: type[BasePluginAppConfig]): + """Checks if plugin is enabled.""" + app_obj = all_plugins_mapped_to_class().get(app) + + return (plugin := self.plugins.get(app_obj.label)) and plugin.state != Plugin.State.DISABLED + + def get_configuration(self, app: type[BasePluginAppConfig]) -> BasePluginConfiguration | None: + """Gets plugin configuration.""" + app_obj = all_plugins_mapped_to_class().get(app) + + return self.configurations.get(app_obj.label) + + @classmethod + def for_changed_conf(cls, section: Section, conf: BasePluginConfiguration) -> SectionsPluginsValidator: + """Creates validator for standard state, but a configuration has been changed.""" + plugin = conf.plugins.get(section=section) + return cls( + section=section, + plugins={p.app_label: p for p in section.plugins.all()}, + configurations={p.app_label: p.configuration for p in section.plugins.all()} | {plugin.app_label: conf}, + ) + + @classmethod + def for_changed_plugin(cls, section: Section, plugin: Plugin) -> SectionsPluginsValidator: + """Creates validator for standard state, but a plugin has been changed.""" + return cls( + section=section, + plugins={p.app_label: p for p in section.plugins.all()} | {plugin.app_label: plugin}, + configurations={p.app_label: p.configuration for p in section.plugins.all()}, + ) diff --git a/fiesta/apps/sections/templates/sections/parts/plugin_state_form.html b/fiesta/apps/sections/templates/sections/parts/plugin_state_form.html index 5980b3f7..88c7e450 100644 --- a/fiesta/apps/sections/templates/sections/parts/plugin_state_form.html +++ b/fiesta/apps/sections/templates/sections/parts/plugin_state_form.html @@ -18,7 +18,8 @@
- {% include "fiestaforms/parts/errors.html" with errors=form.errors %} + {% include "fiestaforms/parts/errors.html" with errors=form.non_field_errors %} + {% for field in form.hidden_fields %}{{ field }}{% endfor %}
diff --git a/fiesta/apps/sections/views/plugins.py b/fiesta/apps/sections/views/plugins.py index 57a04bd2..ae8c0a05 100644 --- a/fiesta/apps/sections/views/plugins.py +++ b/fiesta/apps/sections/views/plugins.py @@ -31,7 +31,10 @@ def by_label(label: str) -> Plugin | None: app, plugin, ( - get_plugin_configuration_form(plugin.configuration)(instance=plugin.configuration) + get_plugin_configuration_form( + configuration=plugin.configuration, + for_section=self.request.in_space_of_section, + )(instance=plugin.configuration) if plugin and plugin.configuration else None ), @@ -62,6 +65,7 @@ class PluginDetailMixin( SuccessMessageMixin, ): model = Plugin + object: Plugin success_url = reverse_lazy("sections:section-plugins") @@ -69,9 +73,18 @@ class PluginDetailMixin( extra_context = { "PluginState": Plugin.State, - "form_url": reverse_lazy("sections:create-plugin"), } + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + + data.update( + { + "form_url": reverse_lazy("sections:change-plugin-state", kwargs={"pk": self.object.pk}), + } + ) + return data + class ChangePluginStateFormView( PluginDetailMixin, @@ -96,7 +109,10 @@ class ChangePluginConfigurationFormView( object: BasePluginConfiguration def get_form_class(self): - return get_plugin_configuration_form(self.object) + return get_plugin_configuration_form( + configuration=self.object, + for_section=self.request.in_space_of_section, + ) queryset = BasePluginConfiguration.objects.all()