From 8b2e2a28d547fc4dbe45515396c660c35d73431f Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sat, 31 Dec 2022 23:20:55 +0100 Subject: [PATCH] [FR] Slack notifications & notification method updates (#4114) * [FR] Slack notification Add slack sending method Fixes #3843 * Add global panel with notification methods * add plugin information * fix plugin lookup and link * Add settings content mixin * Add instructions for Slack setup * fix rendering of custom settings content --- InvenTree/common/notifications.py | 1 + InvenTree/plugin/base/integration/mixins.py | 21 +++++ .../builtin/integration/core_notifications.py | 80 ++++++++++++++++++- InvenTree/plugin/mixins/__init__.py | 5 +- .../plugin/templatetags/plugin_extras.py | 22 +++++ .../settings/mixins/settings_content.html | 6 ++ .../InvenTree/settings/notifications.html | 54 +++++++++++++ .../InvenTree/settings/plugin_settings.html | 2 + .../InvenTree/settings/settings.html | 1 + .../templates/InvenTree/settings/sidebar.html | 2 + 10 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 InvenTree/templates/InvenTree/settings/mixins/settings_content.html create mode 100644 InvenTree/templates/InvenTree/settings/notifications.html diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index af9b6aa21c..db8c5d11a7 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -192,6 +192,7 @@ class MethodStorageClass: for item in current_method: plugin = item.get_plugin(item) ref = f'{plugin.package_path}_{item.METHOD_NAME}' if plugin else item.METHOD_NAME + item.plugin = plugin() if plugin else None filtered_list[ref] = item storage.liste = list(filtered_list.values()) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 232394ca3d..97ca82ea15 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -742,3 +742,24 @@ class PanelMixin: panels.append(panel) return panels + + +class SettingsContentMixin: + """Mixin which allows integration of custom HTML content into a plugins settings page. + + The 'get_settings_content' method must return the HTML content to appear in the section + """ + + class MixinMeta: + """Meta for mixin.""" + + MIXIN_NAME = 'SettingsContent' + + def __init__(self): + """Register mixin.""" + super().__init__() + self.add_mixin('settingscontent', True, __class__) + + def get_settings_content(self, view, request): + """This method *must* be implemented by the plugin class.""" + raise MixinNotImplementedError(f"{__class__} is missing the 'get_settings_content' method") diff --git a/InvenTree/plugin/builtin/integration/core_notifications.py b/InvenTree/plugin/builtin/integration/core_notifications.py index 67ca3e498e..9d57208875 100644 --- a/InvenTree/plugin/builtin/integration/core_notifications.py +++ b/InvenTree/plugin/builtin/integration/core_notifications.py @@ -3,13 +3,15 @@ from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ +import requests from allauth.account.models import EmailAddress import common.models import InvenTree.helpers import InvenTree.tasks -from plugin import InvenTreePlugin -from plugin.mixins import BulkNotificationMethod, SettingsMixin +from plugin import InvenTreePlugin, registry +from plugin.mixins import (BulkNotificationMethod, SettingsContentMixin, + SettingsMixin) class PlgMixin: @@ -23,7 +25,7 @@ class PlgMixin: return CoreNotificationsPlugin -class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin): +class CoreNotificationsPlugin(SettingsContentMixin, SettingsMixin, InvenTreePlugin): """Core notification methods for InvenTree.""" NAME = "CoreNotificationsPlugin" @@ -39,8 +41,30 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin): 'default': False, 'validator': bool, }, + 'ENABLE_NOTIFICATION_SLACK': { + 'name': _('Enable slack notifications'), + 'description': _('Allow sending of slack channel messages for event notifications'), + 'default': False, + 'validator': bool, + }, + 'NOTIFICATION_SLACK_URL': { + 'name': _('Slack incoming webhook url'), + 'description': _('URL that is used to send messages to a slack channel'), + 'protected': True, + }, } + def get_settings_content(self, request): + """Custom settings content for the plugin.""" + return """ +

Setup for Slack:

+
    +
  1. Create a new Slack app on this page
  2. +
  3. Enable Incoming Webhooks for the channel you want the notifications posted to
  4. +
  5. Set the webhook URL in the settings above
  6. +
  7. Enable the plugin
  8. + """ + class EmailNotification(PlgMixin, BulkNotificationMethod): """Notificationmethod for delivery via Email.""" @@ -94,3 +118,53 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin): InvenTree.tasks.send_email(subject, '', targets, html_message=html_message) return True + + class SlackNotification(PlgMixin, BulkNotificationMethod): + """Notificationmethod for delivery via Slack channel messages.""" + + METHOD_NAME = 'slack' + METHOD_ICON = 'fa-envelope' + GLOBAL_SETTING = 'ENABLE_NOTIFICATION_SLACK' + + def get_targets(self): + """Not used by this method.""" + return self.targets + + def send_bulk(self): + """Send the notifications out via slack.""" + + instance = registry.plugins.get(self.get_plugin().NAME.lower()) + url = instance.get_setting('NOTIFICATION_SLACK_URL') + + if not url: + return False + + ret = requests.post(url, json={ + 'text': str(self.context['message']), + 'blocks': [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": str(self.context['name']) + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": str(self.context['message']) + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": str(_("Open link")), "emoji": True + }, + "value": f'{self.category}_{self.obj.pk}', + "url": self.context['link'], + "action_id": "button-action" + } + }] + }) + return ret.ok diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index df4b9dcb0f..c42c57b528 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -8,8 +8,8 @@ from ..base.barcodes.mixins import BarcodeMixin from ..base.event.mixins import EventMixin from ..base.integration.mixins import (APICallMixin, AppMixin, NavigationMixin, PanelMixin, ScheduleMixin, - SettingsMixin, UrlsMixin, - ValidationMixin) + SettingsContentMixin, SettingsMixin, + UrlsMixin, ValidationMixin) from ..base.label.mixins import LabelPrintingMixin from ..base.locate.mixins import LocateMixin @@ -20,6 +20,7 @@ __all__ = [ 'LabelPrintingMixin', 'NavigationMixin', 'ScheduleMixin', + 'SettingsContentMixin', 'SettingsMixin', 'UrlsMixin', 'PanelMixin', diff --git a/InvenTree/plugin/templatetags/plugin_extras.py b/InvenTree/plugin/templatetags/plugin_extras.py index 94ccef8116..4d34d264e6 100644 --- a/InvenTree/plugin/templatetags/plugin_extras.py +++ b/InvenTree/plugin/templatetags/plugin_extras.py @@ -29,6 +29,15 @@ def plugin_settings(plugin, *args, **kwargs): return registry.mixins_settings.get(plugin) +@register.simple_tag(takes_context=True) +def plugin_settings_content(context, plugin, *args, **kwargs): + """Get the settings content for the plugin.""" + plg = registry.get_plugin(plugin) + if hasattr(plg, 'get_settings_content'): + return plg.get_settings_content(context.request) + return None + + @register.simple_tag() def mixin_enabled(plugin, key, *args, **kwargs): """Is the mixin registerd and configured in the plugin?""" @@ -71,3 +80,16 @@ def plugin_errors(*args, **kwargs): def notification_settings_list(context, *args, **kwargs): """List of all user notification settings.""" return storage.get_usersettings(user=context.get('user', None)) + + +@register.simple_tag(takes_context=True) +def notification_list(context, *args, **kwargs): + """List of all notification methods.""" + return [{ + 'slug': a.METHOD_NAME, + 'icon': a.METHOD_ICON, + 'setting': a.GLOBAL_SETTING, + 'plugin': a.plugin, + 'description': a.__doc__, + 'name': a.__name__ + } for a in storage.liste] diff --git a/InvenTree/templates/InvenTree/settings/mixins/settings_content.html b/InvenTree/templates/InvenTree/settings/mixins/settings_content.html new file mode 100644 index 0000000000..956ef56a40 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/mixins/settings_content.html @@ -0,0 +1,6 @@ +{% load plugin_extras %} + +{% plugin_settings_content plugin_key as plugin_settings_content %} +{% if plugin_settings_content %} +{{ plugin_settings_content|safe }} +{% endif %} diff --git a/InvenTree/templates/InvenTree/settings/notifications.html b/InvenTree/templates/InvenTree/settings/notifications.html new file mode 100644 index 0000000000..fac6d22c51 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/notifications.html @@ -0,0 +1,54 @@ +{% extends "panel.html" %} + +{% load i18n %} +{% load inventree_extras %} +{% load plugin_extras %} + +{% block label %}global-notifications{% endblock label %} + +{% block heading %}{% trans "Global Notification Settings" %}{% endblock heading %} + +{% block content %} + +
    + + + + + + + + + {% notification_list as methods %} + {% for method in methods %} + + + + + + + {% if method.setting %} + + + + + {% endif %} + {% endfor %} + +
    {% trans "Name" %}{% trans "Slug" %}{% trans "Description" %}
    {% if method.icon %}{% endif %} + {{ method.name }} + {% if method.plugin %} + + {{ method.plugin.slug }} + + {% endif %} + {{ method.slug }}{{ method.description }}
    + + + {% include "InvenTree/settings/setting.html" with key=method.setting plugin=method.plugin %} + +
    +
    +
    + +{% endblock content %} diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index 1994ac872a..6a095ead2d 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -148,4 +148,6 @@ {% include 'InvenTree/settings/mixins/urls.html' %} {% endif %} +{% include 'InvenTree/settings/mixins/settings_content.html' %} + {% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index 7e553ad0a1..83939c418b 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -32,6 +32,7 @@ {% include "InvenTree/settings/global.html" %} {% include "InvenTree/settings/login.html" %} {% include "InvenTree/settings/barcode.html" %} +{% include "InvenTree/settings/notifications.html" %} {% include "InvenTree/settings/label.html" %} {% include "InvenTree/settings/report.html" %} {% include "InvenTree/settings/part.html" %} diff --git a/InvenTree/templates/InvenTree/settings/sidebar.html b/InvenTree/templates/InvenTree/settings/sidebar.html index 57070f463c..49826e0be8 100644 --- a/InvenTree/templates/InvenTree/settings/sidebar.html +++ b/InvenTree/templates/InvenTree/settings/sidebar.html @@ -32,6 +32,8 @@ {% include "sidebar_item.html" with label='login' text=text icon="fa-fingerprint" %} {% trans "Barcode Support" as text %} {% include "sidebar_item.html" with label='barcodes' text=text icon="fa-qrcode" %} +{% trans "Notifications" as text %} +{% include "sidebar_item.html" with label='global-notifications' text=text icon="fa-bell" %} {% trans "Pricing" as text %} {% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %} {% trans "Label Printing" as text %}