diff --git a/alerts/forms.py b/alerts/forms.py index 3afe99a..75ca885 100644 --- a/alerts/forms.py +++ b/alerts/forms.py @@ -3,7 +3,7 @@ from django.forms import ModelForm from .models import MessagingServiceConfig -class MessagingServiceConfigForm(ModelForm): +class MessagingServiceConfigNewForm(ModelForm): def __init__(self, project, *args, **kwargs): super().__init__(*args, **kwargs) @@ -19,3 +19,13 @@ class MessagingServiceConfigForm(ModelForm): if commit: instance.save() return instance + + +class MessagingServiceConfigEditForm(ModelForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + class Meta: + model = MessagingServiceConfig + fields = ["display_name"] diff --git a/alerts/migrations/0004_alter_messagingserviceconfig_kind.py b/alerts/migrations/0004_alter_messagingserviceconfig_kind.py index f380b1d..6c5c3c8 100644 --- a/alerts/migrations/0004_alter_messagingserviceconfig_kind.py +++ b/alerts/migrations/0004_alter_messagingserviceconfig_kind.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): model_name="messagingserviceconfig", name="kind", field=models.CharField( - choices=alerts.models.kind_choices, default="slack", max_length=20 + choices=alerts.models.get_alert_service_kind_choices, default="slack", max_length=20 ), ), ] diff --git a/alerts/models.py b/alerts/models.py index 7d5e623..570542c 100644 --- a/alerts/models.py +++ b/alerts/models.py @@ -6,7 +6,7 @@ from .service_backends.mattermost import MattermostBackend from .service_backends.discord import DiscordBackend -def kind_choices(): +def get_alert_service_kind_choices(): # As a callable to avoid non-DB-affecting migrations for adding new kinds. # Messaging backends don't need translations since they are brand names. return [ @@ -16,12 +16,22 @@ def kind_choices(): ] +def get_alert_service_backend_class(kind): + if kind == "discord": + return DiscordBackend + if kind == "mattermost": + return MattermostBackend + if kind == "slack": + return SlackBackend + raise ValueError(f"Unknown backend kind: {kind}") + + class MessagingServiceConfig(models.Model): project = models.ForeignKey(Project, on_delete=models.DO_NOTHING, related_name="service_configs") display_name = models.CharField(max_length=100, blank=False, help_text='For display in the UI, e.g. "#general on company Slack"') - kind = models.CharField(choices=kind_choices, max_length=20, default="slack") + kind = models.CharField(choices=get_alert_service_kind_choices, max_length=20, default="slack") config = models.TextField(blank=False) @@ -40,13 +50,7 @@ class MessagingServiceConfig(models.Model): help_text="Error message from the exception") def get_backend(self): - if self.kind == "discord": - return DiscordBackend(self) - if self.kind == "mattermost": - return MattermostBackend(self) - if self.kind == "slack": - return SlackBackend(self) - raise ValueError(f"Unknown backend kind: {self.kind}") + return get_alert_service_backend_class(self.kind)(self) def clear_failure_status(self): """Clear all failure tracking fields on successful operation""" diff --git a/alerts/service_backends/discord.py b/alerts/service_backends/discord.py index 7c7b85e..4b2f901 100644 --- a/alerts/service_backends/discord.py +++ b/alerts/service_backends/discord.py @@ -203,7 +203,8 @@ class DiscordBackend: def __init__(self, service_config): self.service_config = service_config - def get_form_class(self): + @classmethod + def get_form_class(cls): return DiscordConfigForm def send_test_message(self): diff --git a/alerts/service_backends/mattermost.py b/alerts/service_backends/mattermost.py index 2b580c6..9942f9b 100644 --- a/alerts/service_backends/mattermost.py +++ b/alerts/service_backends/mattermost.py @@ -185,7 +185,8 @@ class MattermostBackend: def __init__(self, service_config): self.service_config = service_config - def get_form_class(self): + @classmethod + def get_form_class(cls): return MattermostConfigForm def send_test_message(self): diff --git a/alerts/service_backends/slack.py b/alerts/service_backends/slack.py index b124cd2..b318310 100644 --- a/alerts/service_backends/slack.py +++ b/alerts/service_backends/slack.py @@ -222,7 +222,8 @@ class SlackBackend: def __init__(self, service_config): self.service_config = service_config - def get_form_class(self): + @classmethod + def get_form_class(cls): return SlackConfigForm def send_test_message(self): diff --git a/projects/templates/projects/project_messaging_service_new.html b/projects/templates/projects/project_messaging_service_new.html new file mode 100644 index 0000000..d4145ca --- /dev/null +++ b/projects/templates/projects/project_messaging_service_new.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% load static %} +{% load tailwind_forms %} + +{% block title %}Messaging Service · {{ project.name }} · {{ site_title }}{% endblock %} + +{% block content %} + +
+ +
+
+ {% csrf_token %} + + {% if messages %} +
    + {% for message in messages %} + {# if we introduce different levels we can use{% message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %} #} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + +
+

Messaging Service | {{ project.name }}

+
+ + {% for field in form %} + {% tailwind_formfield field %} + {% endfor %} + + {% for config_form_kind, config_form in config_forms.items %} +
+ + {% for field in config_form %} + {% tailwind_formfield field %} + {% endfor %} +
+ {% endfor %} + + + + Cancel + +
+ +
+
+ +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/projects/views.py b/projects/views.py index fb4111b..27e10a4 100644 --- a/projects/views.py +++ b/projects/views.py @@ -20,9 +20,8 @@ from bugsink.app_settings import get_settings, CB_ANYBODY, CB_MEMBERS, CB_ADMINS from bugsink.decorators import login_exempt, atomic_for_request_method from bugsink.utils import assert_ -from alerts.models import MessagingServiceConfig -from alerts.forms import MessagingServiceConfigForm -from alerts.service_backends.slack import SlackConfigForm +from alerts.models import MessagingServiceConfig, get_alert_service_backend_class, get_alert_service_kind_choices +from alerts.forms import MessagingServiceConfigNewForm, MessagingServiceConfigEditForm from .models import Project, ProjectMembership, ProjectRole, ProjectVisibility from .forms import ProjectMembershipForm, MyProjectMembershipForm, ProjectMemberInviteForm, ProjectForm @@ -457,26 +456,35 @@ def project_messaging_service_add(request, project_pk): project = Project.objects.get(id=project_pk, is_deleted=False) _check_project_admin(project, request.user) + config_forms = { + kind: get_alert_service_backend_class(kind).get_form_class()() + for (kind, _) in get_alert_service_kind_choices() + } + if request.method == 'POST': - form = MessagingServiceConfigForm(project, request.POST) - config_form = SlackConfigForm(data=request.POST) + form = MessagingServiceConfigNewForm(project, request.POST) + kind = form.data.get('kind') or form.fields['kind'].initial + config_form = get_alert_service_backend_class(kind).get_form_class()(data=request.POST) + config_forms[kind] = config_form - if form.is_valid() and config_form.is_valid(): - service = form.save(commit=False) - service.config = json.dumps(config_form.get_config()) - service.save() + if form.is_valid(): + if config_form.is_valid(): + service = form.save(commit=False) + service.config = json.dumps(config_form.get_config()) + service.save() - messages.success(request, "Messaging service added successfully.") - return redirect('project_alerts_setup', project_pk=project_pk) + messages.success(request, "Messaging service added successfully.") + return redirect('project_alerts_setup', project_pk=project_pk) else: - form = MessagingServiceConfigForm(project) - config_form = SlackConfigForm() + form = MessagingServiceConfigNewForm(project) + kind = form.fields['kind'].initial - return render(request, 'projects/project_messaging_service_edit.html', { + return render(request, 'projects/project_messaging_service_new.html', { 'project': project, 'form': form, - 'config_form': config_form, + 'config_forms': config_forms, + 'selected_config_form_kind': kind, }) @@ -486,10 +494,14 @@ def project_messaging_service_edit(request, project_pk, service_pk): _check_project_admin(project, request.user) instance = project.service_configs.get(id=service_pk) + # for editing, we don't allow for changing the kind; although it's probably possible to implement it, it would raise + # questions on "how much are the various configs related (should data be transferred from one config to another). + # and even though "it's possible" simply disallowing greatly simplifies the implementation. + config_form_class = get_alert_service_backend_class(instance.kind).get_form_class() if request.method == 'POST': - form = MessagingServiceConfigForm(project, request.POST, instance=instance) - config_form = SlackConfigForm(data=request.POST) + form = MessagingServiceConfigEditForm(request.POST, instance=instance) + config_form = config_form_class(data=request.POST) if form.is_valid() and config_form.is_valid(): service = form.save(commit=False) @@ -500,8 +512,8 @@ def project_messaging_service_edit(request, project_pk, service_pk): return redirect('project_alerts_setup', project_pk=project_pk) else: - form = MessagingServiceConfigForm(project, instance=instance) - config_form = SlackConfigForm(config=json.loads(instance.config)) + form = MessagingServiceConfigEditForm(instance=instance) + config_form = config_form_class(config=json.loads(instance.config)) return render(request, 'projects/project_messaging_service_edit.html', { 'project': project,