Skip to content

Commit

Permalink
feat(plugins): validator for cross-plugin configuration validation
Browse files Browse the repository at this point in the history
  • Loading branch information
thejoeejoee committed Nov 10, 2023
1 parent 5ce1092 commit 0f20403
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 5 deletions.
16 changes: 15 additions & 1 deletion fiesta/apps/sections/forms/plugin_configuration.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
12 changes: 12 additions & 0 deletions fiesta/apps/sections/forms/plugin_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
75 changes: 75 additions & 0 deletions fiesta/apps/sections/services/sections_plugins_validator.py
Original file line number Diff line number Diff line change
@@ -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()},
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

<hr class="mt-4">

{% 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 %}

<div class="Forms__field Forms__field--buttons space-x-4">
Expand Down
22 changes: 19 additions & 3 deletions fiesta/apps/sections/views/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
Expand Down Expand Up @@ -62,16 +65,26 @@ class PluginDetailMixin(
SuccessMessageMixin,
):
model = Plugin
object: Plugin

success_url = reverse_lazy("sections:section-plugins")

ajax_template_name = "sections/parts/plugin_state_form.html"

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,
Expand All @@ -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()

Expand Down

0 comments on commit 0f20403

Please sign in to comment.