Coverage for src/backend/InvenTree/plugin/builtin/integration/core_notifications.py: 73%
70 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Core set of Notifications as a Plugin."""
3from django.contrib.auth.models import User
4from django.db.models import Model
5from django.template.loader import render_to_string
6from django.utils.translation import gettext_lazy as _
8import requests
9import structlog
11import InvenTree.helpers_email
12from common.settings import get_global_setting
13from plugin import InvenTreePlugin
14from plugin.mixins import NotificationMixin, SettingsMixin
16logger = structlog.get_logger('inventree')
19class InvenTreeUINotifications(NotificationMixin, InvenTreePlugin):
20 """Plugin mixin class for supporting UI notification methods."""
22 NAME = 'InvenTreeUINotifications'
23 TITLE = _('InvenTree UI Notifications')
24 SLUG = 'inventree-ui-notification'
25 AUTHOR = _('InvenTree contributors')
26 DESCRIPTION = _('Integrated UI notification methods')
27 VERSION = '1.0.0'
29 def send_notification(
30 self, target: Model, category: str, users: list[User], context: dict
31 ) -> bool:
32 """Create a UI notification entry for specified users."""
33 from common.models import NotificationMessage
35 ctx = context if context else {}
36 entries = []
38 if not users:
39 return False
41 # Ensure that there is always target object - see https://github.com/inventree/InvenTree/issues/10435
42 if not target: 42 ↛ 43line 42 didn't jump to line 43 because the condition on line 42 was never true
43 target = self.plugin_config()
45 # Bulk create notification messages for all provided users
46 for user in users:
47 entries.append(
48 NotificationMessage(
49 target_object=target,
50 source_object=user,
51 user=user,
52 category=category,
53 name=ctx.get('name'),
54 message=ctx.get('message'),
55 )
56 )
58 NotificationMessage.objects.bulk_create(entries, batch_size=250)
60 return True
63class InvenTreeEmailNotifications(NotificationMixin, SettingsMixin, InvenTreePlugin):
64 """Plugin mixin class for supporting email notification methods."""
66 NAME = 'InvenTreeEmailNotifications'
67 TITLE = _('InvenTree Email Notifications')
68 SLUG = 'inventree-email-notification'
69 AUTHOR = _('InvenTree contributors')
70 DESCRIPTION = _('Integrated email notification methods')
71 VERSION = '1.0.0'
73 USER_SETTINGS = {
74 'NOTIFY_BY_EMAIL': {
75 'name': _('Allow email notifications'),
76 'description': _('Allow email notifications to be sent to this user'),
77 'default': True,
78 'validator': bool,
79 }
80 }
82 def send_notification(
83 self, target: Model, category: str, users: list[User], context: dict
84 ) -> bool:
85 """Send notification to the specified targets."""
86 # Ignore if there is no template provided to render
87 if not context.get('template'): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 return False
90 html_message = render_to_string(context['template']['html'], context)
92 # Prefix the 'instance title' to the email subject
93 instance_title = get_global_setting('INVENTREE_INSTANCE')
94 subject = context['template'].get('subject', '')
96 if instance_title: 96 ↛ 99line 96 didn't jump to line 99 because the condition on line 96 was always true
97 subject = f'[{instance_title}] {subject}'
99 recipients = []
101 for user in users: 101 ↛ 103line 101 didn't jump to line 103 because the loop on line 101 never started
102 # Skip if the user does not want to receive email notifications
103 if not self.get_user_setting('NOTIFY_BY_EMAIL', user, backup_value=False):
104 continue
106 if email := InvenTree.helpers_email.get_email_for_user(user):
107 recipients.append(email)
109 if recipients: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 InvenTree.helpers_email.send_email(
111 subject, '', recipients, html_message=html_message
112 )
113 return True
115 # No recipients found, so we cannot send the email
116 return False
119class InvenTreeSlackNotifications(NotificationMixin, SettingsMixin, InvenTreePlugin):
120 """Plugin mixin class for supporting Slack notification methods."""
122 NAME = 'InvenTreeSlackNotifications'
123 TITLE = _('InvenTree Slack Notifications')
124 SLUG = 'inventree-slack-notification'
125 AUTHOR = _('InvenTree contributors')
126 DESCRIPTION = _('Integrated Slack notification methods')
127 VERSION = '1.0.0'
129 SETTINGS = {
130 'NOTIFICATION_SLACK_URL': {
131 'name': _('Slack incoming webhook URL'),
132 'description': _('URL that is used to send messages to a slack channel'),
133 'protected': True,
134 }
135 }
137 def send_notification(
138 self, target: Model, category: str, users: list[User], context: dict
139 ) -> bool:
140 """Send the notifications out via slack."""
141 url = self.get_setting('NOTIFICATION_SLACK_URL')
143 if not url:
144 return False
146 ret = requests.post(
147 url,
148 json={
149 'text': str(context['message']),
150 'blocks': [
151 {
152 'type': 'section',
153 'text': {'type': 'plain_text', 'text': str(context['name'])},
154 },
155 {
156 'type': 'section',
157 'text': {'type': 'mrkdwn', 'text': str(context['message'])},
158 'accessory': {
159 'type': 'button',
160 'text': {
161 'type': 'plain_text',
162 'text': str(_('Open link')),
163 'emoji': True,
164 },
165 'value': f'{category}_{target.pk}' if target else '',
166 'url': context['link'],
167 'action_id': 'button-action',
168 },
169 },
170 ],
171 },
172 )
174 return ret.ok