mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2805 from matmair/matmair/issue2385
Plugins for notifications
This commit is contained in:
commit
9e1d8e52f3
@ -31,6 +31,8 @@ class InvenTreeConfig(AppConfig):
|
|||||||
if not isInTestMode(): # pragma: no cover
|
if not isInTestMode(): # pragma: no cover
|
||||||
self.update_exchange_rates()
|
self.update_exchange_rates()
|
||||||
|
|
||||||
|
self.collect_notification_methods()
|
||||||
|
|
||||||
if canAppAccessDatabase() or settings.TESTING_ENV:
|
if canAppAccessDatabase() or settings.TESTING_ENV:
|
||||||
self.add_user_on_startup()
|
self.add_user_on_startup()
|
||||||
|
|
||||||
@ -197,3 +199,11 @@ class InvenTreeConfig(AppConfig):
|
|||||||
|
|
||||||
# do not try again
|
# do not try again
|
||||||
settings.USER_ADDED = True
|
settings.USER_ADDED = True
|
||||||
|
|
||||||
|
def collect_notification_methods(self):
|
||||||
|
"""
|
||||||
|
Collect all notification methods
|
||||||
|
"""
|
||||||
|
from common.notifications import storage
|
||||||
|
|
||||||
|
storage.collect()
|
||||||
|
@ -24,6 +24,8 @@ from django_q.tasks import async_task
|
|||||||
import common.models
|
import common.models
|
||||||
import common.serializers
|
import common.serializers
|
||||||
from InvenTree.helpers import inheritors
|
from InvenTree.helpers import inheritors
|
||||||
|
from plugin.models import NotificationUserSetting
|
||||||
|
from plugin.serializers import NotificationUserSettingSerializer
|
||||||
|
|
||||||
|
|
||||||
class CsrfExemptMixin(object):
|
class CsrfExemptMixin(object):
|
||||||
@ -145,7 +147,7 @@ class GlobalSettingsPermissions(permissions.BasePermission):
|
|||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
return user.is_staff
|
return user.is_staff
|
||||||
except AttributeError:
|
except AttributeError: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -179,7 +181,7 @@ class UserSettingsList(SettingsList):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
except AttributeError:
|
except AttributeError: # pragma: no cover
|
||||||
return common.models.InvenTreeUserSetting.objects.none()
|
return common.models.InvenTreeUserSetting.objects.none()
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
@ -198,7 +200,7 @@ class UserSettingsPermissions(permissions.BasePermission):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user = request.user
|
user = request.user
|
||||||
except AttributeError:
|
except AttributeError: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return user == obj.user
|
return user == obj.user
|
||||||
@ -219,6 +221,44 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSettingsList(SettingsList):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing a list of notification user settings objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = NotificationUserSetting.objects.all()
|
||||||
|
serializer_class = NotificationUserSettingSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Only list settings which apply to the current user
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = self.request.user
|
||||||
|
except AttributeError:
|
||||||
|
return NotificationUserSetting.objects.none()
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
queryset = queryset.filter(user=user)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||||
|
"""
|
||||||
|
Detail view for an individual "notification user setting" object
|
||||||
|
|
||||||
|
- User can only view / edit settings their own settings objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = NotificationUserSetting.objects.all()
|
||||||
|
serializer_class = NotificationUserSettingSerializer
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
UserSettingsPermissions,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class NotificationList(generics.ListAPIView):
|
class NotificationList(generics.ListAPIView):
|
||||||
queryset = common.models.NotificationMessage.objects.all()
|
queryset = common.models.NotificationMessage.objects.all()
|
||||||
serializer_class = common.serializers.NotificationMessageSerializer
|
serializer_class = common.serializers.NotificationMessageSerializer
|
||||||
@ -344,6 +384,15 @@ settings_api_urls = [
|
|||||||
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
# Notification settings
|
||||||
|
re_path(r'^notification/', include([
|
||||||
|
# Notification Settings Detail
|
||||||
|
re_path(r'^(?P<pk>\d+)/', NotificationUserSettingsDetail.as_view(), name='api-notification-setting-detail'),
|
||||||
|
|
||||||
|
# Notification Settings List
|
||||||
|
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notifcation-setting-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Global settings
|
# Global settings
|
||||||
re_path(r'^global/', include([
|
re_path(r'^global/', include([
|
||||||
# Global Settings Detail
|
# Global Settings Detail
|
||||||
|
@ -265,6 +265,12 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
filters['plugin'] = plugin
|
filters['plugin'] = plugin
|
||||||
kwargs['plugin'] = plugin
|
kwargs['plugin'] = plugin
|
||||||
|
|
||||||
|
# Filter by method
|
||||||
|
method = kwargs.get('method', None)
|
||||||
|
|
||||||
|
if method is not None:
|
||||||
|
filters['method'] = method
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setting = settings.filter(**filters).first()
|
setting = settings.filter(**filters).first()
|
||||||
except (ValueError, cls.DoesNotExist):
|
except (ValueError, cls.DoesNotExist):
|
||||||
@ -648,6 +654,87 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
return self.__class__.is_protected(self.key)
|
return self.__class__.is_protected(self.key)
|
||||||
|
|
||||||
|
|
||||||
|
class GenericReferencedSettingClass:
|
||||||
|
"""
|
||||||
|
This mixin can be used to add reference keys to static properties
|
||||||
|
|
||||||
|
Sample:
|
||||||
|
```python
|
||||||
|
class SampleSetting(GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
||||||
|
class Meta:
|
||||||
|
unique_together = [
|
||||||
|
('sample', 'key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
REFERENCE_NAME = 'sample'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
|
# mysampledict contains the dict with all settings for this SettingClass - this could also be a dynamic lookup
|
||||||
|
|
||||||
|
kwargs['settings'] = mysampledict
|
||||||
|
return super().get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
|
sample = models.charKey( # the name for this field is the additonal key and must be set in the Meta class an REFERENCE_NAME
|
||||||
|
max_length=256,
|
||||||
|
verbose_name=_('sample')
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
REFERENCE_NAME = None
|
||||||
|
|
||||||
|
def _get_reference(self):
|
||||||
|
"""
|
||||||
|
Returns dict that can be used as an argument for kwargs calls.
|
||||||
|
Helps to make overriden calls generic for simple reuse.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
some_random_function(argument0, kwarg1=value1, **self._get_reference())
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
self.REFERENCE_NAME: getattr(self, self.REFERENCE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
We override the following class methods,
|
||||||
|
so that we can pass the modified key instance as an additional argument
|
||||||
|
"""
|
||||||
|
|
||||||
|
def clean(self, **kwargs):
|
||||||
|
|
||||||
|
kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME)
|
||||||
|
|
||||||
|
super().clean(**kwargs)
|
||||||
|
|
||||||
|
def is_bool(self, **kwargs):
|
||||||
|
|
||||||
|
kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME)
|
||||||
|
|
||||||
|
return super().is_bool(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.__class__.get_setting_name(self.key, **self._get_reference())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_value(self):
|
||||||
|
return self.__class__.get_setting_default(self.key, **self._get_reference())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return self.__class__.get_setting_description(self.key, **self._get_reference())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def units(self):
|
||||||
|
return self.__class__.get_setting_units(self.key, **self._get_reference())
|
||||||
|
|
||||||
|
def choices(self):
|
||||||
|
return self.__class__.get_setting_choices(self.key, **self._get_reference())
|
||||||
|
|
||||||
|
|
||||||
def settings_group_options():
|
def settings_group_options():
|
||||||
"""
|
"""
|
||||||
Build up group tuple for settings based on your choices
|
Build up group tuple for settings based on your choices
|
||||||
@ -1299,14 +1386,6 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'default': True,
|
'default': True,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
'NOTIFICATION_SEND_EMAILS': {
|
|
||||||
'name': _('Enable email notifications'),
|
|
||||||
'description': _('Allow sending of emails for event notifications'),
|
|
||||||
'default': True,
|
|
||||||
'validator': bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
'LABEL_ENABLE': {
|
'LABEL_ENABLE': {
|
||||||
'name': _('Enable label printing'),
|
'name': _('Enable label printing'),
|
||||||
'description': _('Enable label printing from the web interface'),
|
'description': _('Enable label printing from the web interface'),
|
||||||
@ -1458,7 +1537,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_object(cls, key, user):
|
def get_setting_object(cls, key, user=None):
|
||||||
return super().get_setting_object(key, user=user)
|
return super().get_setting_object(key, user=user)
|
||||||
|
|
||||||
def validate_unique(self, exclude=None, **kwargs):
|
def validate_unique(self, exclude=None, **kwargs):
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
from allauth.account.models import EmailAddress
|
|
||||||
|
|
||||||
from InvenTree.helpers import inheritors
|
from InvenTree.helpers import inheritors
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
from common.models import NotificationEntry, NotificationMessage
|
from common.models import NotificationEntry, NotificationMessage
|
||||||
from common.models import InvenTreeUserSetting
|
from plugin import registry
|
||||||
|
from plugin.models import NotificationUserSetting
|
||||||
import InvenTree.tasks
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
# region methods
|
||||||
class NotificationMethod:
|
class NotificationMethod:
|
||||||
"""
|
"""
|
||||||
Base class for notification methods
|
Base class for notification methods
|
||||||
"""
|
"""
|
||||||
|
|
||||||
METHOD_NAME = ''
|
METHOD_NAME = ''
|
||||||
|
METHOD_ICON = None
|
||||||
CONTEXT_BUILTIN = ['name', 'message', ]
|
CONTEXT_BUILTIN = ['name', 'message', ]
|
||||||
CONTEXT_EXTRA = []
|
CONTEXT_EXTRA = []
|
||||||
|
GLOBAL_SETTING = None
|
||||||
|
USER_SETTING = None
|
||||||
|
|
||||||
def __init__(self, obj, category, targets, context) -> None:
|
def __init__(self, obj, category, targets, context) -> None:
|
||||||
# Check if a sending fnc is defined
|
# Check if a sending fnc is defined
|
||||||
@ -34,6 +33,11 @@ class NotificationMethod:
|
|||||||
if self.METHOD_NAME in ('', None):
|
if self.METHOD_NAME in ('', None):
|
||||||
raise NotImplementedError(f'The NotificationMethod {self.__class__} did not provide a METHOD_NAME')
|
raise NotImplementedError(f'The NotificationMethod {self.__class__} did not provide a METHOD_NAME')
|
||||||
|
|
||||||
|
# Check if plugin is disabled - if so do not gather targets etc.
|
||||||
|
if self.global_setting_disable():
|
||||||
|
self.targets = None
|
||||||
|
return
|
||||||
|
|
||||||
# Define arguments
|
# Define arguments
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self.category = category
|
self.category = category
|
||||||
@ -84,12 +88,40 @@ class NotificationMethod:
|
|||||||
def setup(self):
|
def setup(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# def send(self, targets)
|
|
||||||
# def send_bulk(self)
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# region plugins
|
||||||
|
def get_plugin(self):
|
||||||
|
"""Returns plugin class"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def global_setting_disable(self):
|
||||||
|
"""Check if the method is defined in a plugin and has a global setting"""
|
||||||
|
# Check if plugin has a setting
|
||||||
|
if not self.GLOBAL_SETTING:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if plugin is set
|
||||||
|
plg_cls = self.get_plugin()
|
||||||
|
if not plg_cls:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if method globally enabled
|
||||||
|
plg_instance = registry.plugins.get(plg_cls.PLUGIN_NAME.lower())
|
||||||
|
if plg_instance and not plg_instance.get_setting(self.GLOBAL_SETTING):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Lets go!
|
||||||
|
return False
|
||||||
|
|
||||||
|
def usersetting(self, target):
|
||||||
|
"""
|
||||||
|
Returns setting for this method for a given user
|
||||||
|
"""
|
||||||
|
return NotificationUserSetting.get_setting(f'NOTIFICATION_METHOD_{self.METHOD_NAME.upper()}', user=target, method=self.METHOD_NAME)
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
class SingleNotificationMethod(NotificationMethod):
|
class SingleNotificationMethod(NotificationMethod):
|
||||||
def send(self, target):
|
def send(self, target):
|
||||||
@ -99,41 +131,59 @@ class SingleNotificationMethod(NotificationMethod):
|
|||||||
class BulkNotificationMethod(NotificationMethod):
|
class BulkNotificationMethod(NotificationMethod):
|
||||||
def send_bulk(self):
|
def send_bulk(self):
|
||||||
raise NotImplementedError('The `send` method must be overriden!')
|
raise NotImplementedError('The `send` method must be overriden!')
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
class EmailNotification(BulkNotificationMethod):
|
class MethodStorageClass:
|
||||||
METHOD_NAME = 'mail'
|
liste = None
|
||||||
CONTEXT_EXTRA = [
|
user_settings = {}
|
||||||
('template', ),
|
|
||||||
('template', 'html', ),
|
|
||||||
('template', 'subject', ),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_targets(self):
|
def collect(self, selected_classes=None):
|
||||||
"""
|
logger.info('collecting notification methods')
|
||||||
Return a list of target email addresses,
|
current_method = inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
|
||||||
only for users which allow email notifications
|
|
||||||
"""
|
|
||||||
|
|
||||||
allowed_users = []
|
# for testing selective loading is made available
|
||||||
|
if selected_classes:
|
||||||
|
current_method = [item for item in current_method if item is selected_classes]
|
||||||
|
|
||||||
for user in self.targets:
|
# make sure only one of each method is added
|
||||||
allows_emails = InvenTreeUserSetting.get_setting('NOTIFICATION_SEND_EMAILS', user=user)
|
filtered_list = {}
|
||||||
|
for item in current_method:
|
||||||
|
plugin = item.get_plugin(item)
|
||||||
|
ref = f'{plugin.package_path}_{item.METHOD_NAME}' if plugin else item.METHOD_NAME
|
||||||
|
filtered_list[ref] = item
|
||||||
|
|
||||||
if allows_emails:
|
storage.liste = list(filtered_list.values())
|
||||||
allowed_users.append(user)
|
logger.info(f'found {len(storage.liste)} notification methods')
|
||||||
|
|
||||||
return EmailAddress.objects.filter(
|
def get_usersettings(self, user):
|
||||||
user__in=allowed_users,
|
methods = []
|
||||||
|
for item in storage.liste:
|
||||||
|
if item.USER_SETTING:
|
||||||
|
new_key = f'NOTIFICATION_METHOD_{item.METHOD_NAME.upper()}'
|
||||||
|
|
||||||
|
# make sure the setting exists
|
||||||
|
self.user_settings[new_key] = item.USER_SETTING
|
||||||
|
NotificationUserSetting.get_setting(
|
||||||
|
key=new_key,
|
||||||
|
user=user,
|
||||||
|
method=item.METHOD_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_bulk(self):
|
# save definition
|
||||||
html_message = render_to_string(self.context['template']['html'], self.context)
|
methods.append({
|
||||||
targets = self.get_targets().values_list('email', flat=True)
|
'key': new_key,
|
||||||
|
'icon': getattr(item, 'METHOD_ICON', ''),
|
||||||
|
'method': item.METHOD_NAME,
|
||||||
|
})
|
||||||
|
return methods
|
||||||
|
|
||||||
InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message)
|
|
||||||
|
|
||||||
return True
|
IGNORED_NOTIFICATION_CLS = set([
|
||||||
|
SingleNotificationMethod,
|
||||||
|
BulkNotificationMethod,
|
||||||
|
])
|
||||||
|
storage = MethodStorageClass()
|
||||||
|
|
||||||
|
|
||||||
class UIMessageNotification(SingleNotificationMethod):
|
class UIMessageNotification(SingleNotificationMethod):
|
||||||
@ -198,9 +248,11 @@ def trigger_notifaction(obj, category=None, obj_ref='pk', **kwargs):
|
|||||||
|
|
||||||
# Collect possible methods
|
# Collect possible methods
|
||||||
if delivery_methods is None:
|
if delivery_methods is None:
|
||||||
delivery_methods = inheritors(NotificationMethod)
|
delivery_methods = storage.liste
|
||||||
|
else:
|
||||||
|
delivery_methods = (delivery_methods - IGNORED_NOTIFICATION_CLS)
|
||||||
|
|
||||||
for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]:
|
for method in delivery_methods:
|
||||||
logger.info(f"Triggering method '{method.METHOD_NAME}'")
|
logger.info(f"Triggering method '{method.METHOD_NAME}'")
|
||||||
try:
|
try:
|
||||||
deliver_notification(method, obj, category, targets, context)
|
deliver_notification(method, obj, category, targets, context)
|
||||||
|
@ -99,6 +99,43 @@ class UserSettingsSerializer(SettingsSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GenericReferencedSettingSerializer(SettingsSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for a GenericReferencedSetting model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
MODEL: model class for the serializer
|
||||||
|
EXTRA_FIELDS: fields that need to be appended to the serializer
|
||||||
|
field must also be defined in the custom class
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODEL = None
|
||||||
|
EXTRA_FIELDS = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Init overrides the Meta class to make it dynamic"""
|
||||||
|
class CustomMeta:
|
||||||
|
"""Scaffold for custom Meta class"""
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'key',
|
||||||
|
'value',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'type',
|
||||||
|
'choices',
|
||||||
|
]
|
||||||
|
|
||||||
|
# set Meta class
|
||||||
|
self.Meta = CustomMeta
|
||||||
|
self.Meta.model = self.MODEL
|
||||||
|
# extend the fields
|
||||||
|
self.Meta.fields.extend(self.EXTRA_FIELDS)
|
||||||
|
|
||||||
|
# resume operations
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class NotificationMessageSerializer(InvenTreeModelSerializer):
|
class NotificationMessageSerializer(InvenTreeModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for the InvenTreeUserSetting model
|
Serializer for the InvenTreeUserSetting model
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod
|
from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage
|
||||||
|
from plugin.models import NotificationUserSetting
|
||||||
from part.test_part import BaseNotificationIntegrationTest
|
from part.test_part import BaseNotificationIntegrationTest
|
||||||
|
import plugin.templatetags.plugin_extras as plugin_tags
|
||||||
|
|
||||||
|
|
||||||
class BaseNotificationTests(BaseNotificationIntegrationTest):
|
class BaseNotificationTests(BaseNotificationIntegrationTest):
|
||||||
@ -36,15 +38,6 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
|||||||
def send(self):
|
def send(self):
|
||||||
"""a comment so we do not need a pass"""
|
"""a comment so we do not need a pass"""
|
||||||
|
|
||||||
class WrongDeliveryImplementation(SingleNotificationMethod):
|
|
||||||
METHOD_NAME = 'WrongDeliveryImplementation'
|
|
||||||
|
|
||||||
def get_targets(self):
|
|
||||||
return [1, ]
|
|
||||||
|
|
||||||
def send(self, target):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# no send / send bulk
|
# no send / send bulk
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
FalseNotificationMethod('', '', '', '', )
|
FalseNotificationMethod('', '', '', '', )
|
||||||
@ -77,13 +70,16 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
|||||||
def send(self, target):
|
def send(self, target):
|
||||||
raise KeyError('This could be any error')
|
raise KeyError('This could be any error')
|
||||||
|
|
||||||
self._notification_run()
|
self._notification_run(ErrorImplementation)
|
||||||
|
|
||||||
|
|
||||||
class BulkNotificationMethodTests(BaseNotificationIntegrationTest):
|
class BulkNotificationMethodTests(BaseNotificationIntegrationTest):
|
||||||
|
|
||||||
def test_BulkNotificationMethod(self):
|
def test_BulkNotificationMethod(self):
|
||||||
"""ensure the implementation requirements are tested"""
|
"""
|
||||||
|
Ensure the implementation requirements are tested.
|
||||||
|
NotImplementedError needs to raise if the send_bulk() method is not set.
|
||||||
|
"""
|
||||||
|
|
||||||
class WrongImplementation(BulkNotificationMethod):
|
class WrongImplementation(BulkNotificationMethod):
|
||||||
METHOD_NAME = 'WrongImplementationBulk'
|
METHOD_NAME = 'WrongImplementationBulk'
|
||||||
@ -92,13 +88,16 @@ class BulkNotificationMethodTests(BaseNotificationIntegrationTest):
|
|||||||
return [1, ]
|
return [1, ]
|
||||||
|
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self._notification_run()
|
self._notification_run(WrongImplementation)
|
||||||
|
|
||||||
|
|
||||||
class SingleNotificationMethodTests(BaseNotificationIntegrationTest):
|
class SingleNotificationMethodTests(BaseNotificationIntegrationTest):
|
||||||
|
|
||||||
def test_SingleNotificationMethod(self):
|
def test_SingleNotificationMethod(self):
|
||||||
"""ensure the implementation requirements are tested"""
|
"""
|
||||||
|
Ensure the implementation requirements are tested.
|
||||||
|
NotImplementedError needs to raise if the send() method is not set.
|
||||||
|
"""
|
||||||
|
|
||||||
class WrongImplementation(SingleNotificationMethod):
|
class WrongImplementation(SingleNotificationMethod):
|
||||||
METHOD_NAME = 'WrongImplementationSingle'
|
METHOD_NAME = 'WrongImplementationSingle'
|
||||||
@ -107,6 +106,51 @@ class SingleNotificationMethodTests(BaseNotificationIntegrationTest):
|
|||||||
return [1, ]
|
return [1, ]
|
||||||
|
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self._notification_run()
|
self._notification_run(WrongImplementation)
|
||||||
|
|
||||||
# A integration test for notifications is provided in test_part.PartNotificationTest
|
# A integration test for notifications is provided in test_part.PartNotificationTest
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSettingTests(BaseNotificationIntegrationTest):
|
||||||
|
""" Tests for NotificationUserSetting """
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.client.login(username=self.user.username, password='password')
|
||||||
|
|
||||||
|
def test_setting_attributes(self):
|
||||||
|
"""check notification method plugin methods: usersettings and tags """
|
||||||
|
|
||||||
|
class SampleImplementation(BulkNotificationMethod):
|
||||||
|
METHOD_NAME = 'test'
|
||||||
|
GLOBAL_SETTING = 'ENABLE_NOTIFICATION_TEST'
|
||||||
|
USER_SETTING = {
|
||||||
|
'name': 'Enable test notifications',
|
||||||
|
'description': 'Allow sending of test for event notifications',
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
'units': 'alpha',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_targets(self):
|
||||||
|
return [1, ]
|
||||||
|
|
||||||
|
def send_bulk(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# run thorugh notification
|
||||||
|
self._notification_run(SampleImplementation)
|
||||||
|
# make sure the array fits
|
||||||
|
array = storage.get_usersettings(self.user)
|
||||||
|
setting = NotificationUserSetting.objects.all().first()
|
||||||
|
|
||||||
|
# assertions for settings
|
||||||
|
self.assertEqual(setting.name, 'Enable test notifications')
|
||||||
|
self.assertEqual(setting.default_value, True)
|
||||||
|
self.assertEqual(setting.description, 'Allow sending of test for event notifications')
|
||||||
|
self.assertEqual(setting.units, 'alpha')
|
||||||
|
|
||||||
|
# test tag and array
|
||||||
|
self.assertEqual(plugin_tags.notification_settings_list({'user': self.user}), array)
|
||||||
|
self.assertEqual(array[0]['key'], 'NOTIFICATION_METHOD_TEST')
|
||||||
|
self.assertEqual(array[0]['method'], 'test')
|
||||||
|
@ -10,6 +10,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
|
from plugin.models import NotificationUserSetting
|
||||||
|
|
||||||
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
|
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
|
||||||
from .api import WebhookView
|
from .api import WebhookView
|
||||||
@ -377,6 +378,31 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
|
||||||
|
"""Tests for the notification user settings API"""
|
||||||
|
|
||||||
|
def test_api_list(self):
|
||||||
|
"""Test list URL"""
|
||||||
|
url = reverse('api-notifcation-setting-list')
|
||||||
|
|
||||||
|
self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
def test_setting(self):
|
||||||
|
"""Test the string name for NotificationUserSetting"""
|
||||||
|
test_setting = NotificationUserSetting.get_setting_object('NOTIFICATION_METHOD_MAIL', user=self.user)
|
||||||
|
self.assertEqual(str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): ')
|
||||||
|
|
||||||
|
|
||||||
|
class PluginSettingsApiTest(InvenTreeAPITestCase):
|
||||||
|
"""Tests for the plugin settings API"""
|
||||||
|
|
||||||
|
def test_api_list(self):
|
||||||
|
"""Test list URL"""
|
||||||
|
url = reverse('api-plugin-setting-list')
|
||||||
|
|
||||||
|
self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
|
||||||
class WebhookMessageTests(TestCase):
|
class WebhookMessageTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.endpoint_def = WebhookEndpoint.objects.create()
|
self.endpoint_def = WebhookEndpoint.objects.create()
|
||||||
@ -489,7 +515,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
assert message.body == {"this": "is a message"}
|
assert message.body == {"this": "is a message"}
|
||||||
|
|
||||||
|
|
||||||
class NotificationTest(TestCase):
|
class NotificationTest(InvenTreeAPITestCase):
|
||||||
|
|
||||||
def test_check_notification_entries(self):
|
def test_check_notification_entries(self):
|
||||||
|
|
||||||
@ -508,6 +534,11 @@ class NotificationTest(TestCase):
|
|||||||
|
|
||||||
self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta))
|
self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta))
|
||||||
|
|
||||||
|
def test_api_list(self):
|
||||||
|
"""Test list URL"""
|
||||||
|
url = reverse('api-notifications-list')
|
||||||
|
self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
|
||||||
class LoadingTest(TestCase):
|
class LoadingTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -28,7 +28,7 @@ import InvenTree.helpers
|
|||||||
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
|
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
from plugin.models import PluginSetting
|
from plugin.models import PluginSetting, NotificationUserSetting
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@ -313,6 +313,9 @@ def setting_object(key, *args, **kwargs):
|
|||||||
|
|
||||||
return PluginSetting.get_setting_object(key, plugin=plugin)
|
return PluginSetting.get_setting_object(key, plugin=plugin)
|
||||||
|
|
||||||
|
if 'method' in kwargs:
|
||||||
|
return NotificationUserSetting.get_setting_object(key, user=kwargs['user'], method=kwargs['method'])
|
||||||
|
|
||||||
if 'user' in kwargs:
|
if 'user' in kwargs:
|
||||||
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
|
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
|
||||||
|
|
||||||
@ -326,6 +329,8 @@ def settings_value(key, *args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if 'user' in kwargs:
|
if 'user' in kwargs:
|
||||||
|
if not kwargs['user']:
|
||||||
|
return InvenTreeUserSetting.get_setting(key)
|
||||||
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||||
|
|
||||||
return InvenTreeSetting.get_setting(key)
|
return InvenTreeSetting.get_setting(key)
|
||||||
|
@ -20,6 +20,7 @@ import part.settings
|
|||||||
|
|
||||||
from InvenTree import version
|
from InvenTree import version
|
||||||
from common.models import InvenTreeSetting, NotificationEntry, NotificationMessage
|
from common.models import InvenTreeSetting, NotificationEntry, NotificationMessage
|
||||||
|
from common.notifications import storage, UIMessageNotification
|
||||||
|
|
||||||
|
|
||||||
class TemplateTagTest(TestCase):
|
class TemplateTagTest(TestCase):
|
||||||
@ -565,7 +566,14 @@ class BaseNotificationIntegrationTest(TestCase):
|
|||||||
# Define part that will be tested
|
# Define part that will be tested
|
||||||
self.part = Part.objects.get(name='R_2K2_0805')
|
self.part = Part.objects.get(name='R_2K2_0805')
|
||||||
|
|
||||||
def _notification_run(self):
|
def _notification_run(self, run_class=None):
|
||||||
|
"""
|
||||||
|
Run a notification test suit through.
|
||||||
|
If you only want to test one class pass it to run_class
|
||||||
|
"""
|
||||||
|
# reload notification methods
|
||||||
|
storage.collect(run_class)
|
||||||
|
|
||||||
# There should be no notification runs
|
# There should be no notification runs
|
||||||
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
||||||
|
|
||||||
@ -588,7 +596,7 @@ class PartNotificationTest(BaseNotificationIntegrationTest):
|
|||||||
""" Integration test for part notifications """
|
""" Integration test for part notifications """
|
||||||
|
|
||||||
def test_notification(self):
|
def test_notification(self):
|
||||||
self._notification_run()
|
self._notification_run(UIMessageNotification)
|
||||||
|
|
||||||
# There should be 1 notification message right now
|
# There should be 1 notification message right now
|
||||||
self.assertEqual(NotificationMessage.objects.all().count(), 1)
|
self.assertEqual(NotificationMessage.objects.all().count(), 1)
|
||||||
|
@ -70,4 +70,20 @@ class PluginConfigAdmin(admin.ModelAdmin):
|
|||||||
inlines = [PluginSettingInline, ]
|
inlines = [PluginSettingInline, ]
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSettingAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Admin class for NotificationUserSetting
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = models.NotificationUserSetting
|
||||||
|
|
||||||
|
read_only_fields = [
|
||||||
|
'key',
|
||||||
|
]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(models.PluginConfig, PluginConfigAdmin)
|
admin.site.register(models.PluginConfig, PluginConfigAdmin)
|
||||||
|
admin.site.register(models.NotificationUserSetting, NotificationUserSettingAdmin)
|
||||||
|
76
InvenTree/plugin/builtin/integration/core_notifications.py
Normal file
76
InvenTree/plugin/builtin/integration/core_notifications.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Core set of Notifications as a Plugin"""
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from allauth.account.models import EmailAddress
|
||||||
|
|
||||||
|
from plugin import IntegrationPluginBase
|
||||||
|
from plugin.mixins import BulkNotificationMethod, SettingsMixin
|
||||||
|
import InvenTree.tasks
|
||||||
|
|
||||||
|
|
||||||
|
class PlgMixin:
|
||||||
|
def get_plugin(self):
|
||||||
|
return CoreNotificationsPlugin
|
||||||
|
|
||||||
|
|
||||||
|
class CoreNotificationsPlugin(SettingsMixin, IntegrationPluginBase):
|
||||||
|
"""
|
||||||
|
Core notification methods for InvenTree
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUGIN_NAME = "CoreNotificationsPlugin"
|
||||||
|
AUTHOR = _('InvenTree contributors')
|
||||||
|
DESCRIPTION = _('Integrated outgoing notificaton methods')
|
||||||
|
|
||||||
|
SETTINGS = {
|
||||||
|
'ENABLE_NOTIFICATION_EMAILS': {
|
||||||
|
'name': _('Enable email notifications'),
|
||||||
|
'description': _('Allow sending of emails for event notifications'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailNotification(PlgMixin, BulkNotificationMethod):
|
||||||
|
METHOD_NAME = 'mail'
|
||||||
|
METHOD_ICON = 'fa-envelope'
|
||||||
|
CONTEXT_EXTRA = [
|
||||||
|
('template', ),
|
||||||
|
('template', 'html', ),
|
||||||
|
('template', 'subject', ),
|
||||||
|
]
|
||||||
|
GLOBAL_SETTING = 'ENABLE_NOTIFICATION_EMAILS'
|
||||||
|
USER_SETTING = {
|
||||||
|
'name': _('Enable email notifications'),
|
||||||
|
'description': _('Allow sending of emails for event notifications'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_targets(self):
|
||||||
|
"""
|
||||||
|
Return a list of target email addresses,
|
||||||
|
only for users which allow email notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
allowed_users = []
|
||||||
|
|
||||||
|
for user in self.targets:
|
||||||
|
allows_emails = self.usersetting(user)
|
||||||
|
|
||||||
|
if allows_emails:
|
||||||
|
allowed_users.append(user)
|
||||||
|
|
||||||
|
return EmailAddress.objects.filter(
|
||||||
|
user__in=allowed_users,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_bulk(self):
|
||||||
|
html_message = render_to_string(self.context['template']['html'], self.context)
|
||||||
|
targets = self.targets.values_list('email', flat=True)
|
||||||
|
|
||||||
|
InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message)
|
||||||
|
|
||||||
|
return True
|
@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from plugin.models import NotificationUserSetting
|
||||||
|
from part.test_part import BaseNotificationIntegrationTest
|
||||||
|
from plugin.builtin.integration.core_notifications import CoreNotificationsPlugin
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
|
||||||
|
class CoreNotificationTestTests(BaseNotificationIntegrationTest):
|
||||||
|
|
||||||
|
def test_email(self):
|
||||||
|
"""
|
||||||
|
Ensure that the email notifications run
|
||||||
|
"""
|
||||||
|
|
||||||
|
# enable plugin and set mail setting to true
|
||||||
|
plugin = registry.plugins.get('corenotificationsplugin')
|
||||||
|
plugin.set_setting('ENABLE_NOTIFICATION_EMAILS', True)
|
||||||
|
NotificationUserSetting.set_setting(
|
||||||
|
key='NOTIFICATION_METHOD_MAIL',
|
||||||
|
value=True,
|
||||||
|
change_user=self.user,
|
||||||
|
user=self.user,
|
||||||
|
method=CoreNotificationsPlugin.EmailNotification.METHOD_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
# run through
|
||||||
|
self._notification_run(CoreNotificationsPlugin.EmailNotification)
|
29
InvenTree/plugin/migrations/0005_notificationusersetting.py
Normal file
29
InvenTree/plugin/migrations/0005_notificationusersetting.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-04-03 23:38
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('plugin', '0004_alter_pluginsetting_key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NotificationUserSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('key', models.CharField(help_text='Settings key (must be unique - case insensitive)', max_length=50)),
|
||||||
|
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
|
||||||
|
('method', models.CharField(max_length=255, verbose_name='Method')),
|
||||||
|
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('method', 'user', 'key')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -3,6 +3,7 @@ Utility class to enable simpler imports
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin
|
from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin
|
||||||
|
from common.notifications import SingleNotificationMethod, BulkNotificationMethod
|
||||||
|
|
||||||
from ..builtin.action.mixins import ActionMixin
|
from ..builtin.action.mixins import ActionMixin
|
||||||
from ..builtin.barcode.mixins import BarcodeMixin
|
from ..builtin.barcode.mixins import BarcodeMixin
|
||||||
@ -18,4 +19,6 @@ __all__ = [
|
|||||||
'UrlsMixin',
|
'UrlsMixin',
|
||||||
'ActionMixin',
|
'ActionMixin',
|
||||||
'BarcodeMixin',
|
'BarcodeMixin',
|
||||||
|
'SingleNotificationMethod',
|
||||||
|
'BulkNotificationMethod',
|
||||||
]
|
]
|
||||||
|
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ class PluginConfig(models.Model):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class PluginSetting(common.models.BaseInvenTreeSetting):
|
class PluginSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
||||||
"""
|
"""
|
||||||
This model represents settings for individual plugins
|
This model represents settings for individual plugins
|
||||||
"""
|
"""
|
||||||
@ -111,41 +112,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
|||||||
('plugin', 'key'),
|
('plugin', 'key'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self, **kwargs):
|
REFERENCE_NAME = 'plugin'
|
||||||
|
|
||||||
kwargs['plugin'] = self.plugin
|
|
||||||
|
|
||||||
super().clean(**kwargs)
|
|
||||||
|
|
||||||
"""
|
|
||||||
We override the following class methods,
|
|
||||||
so that we can pass the plugin instance
|
|
||||||
"""
|
|
||||||
|
|
||||||
def is_bool(self, **kwargs):
|
|
||||||
|
|
||||||
kwargs['plugin'] = self.plugin
|
|
||||||
|
|
||||||
return super().is_bool(**kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.__class__.get_setting_name(self.key, plugin=self.plugin)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def default_value(self):
|
|
||||||
return self.__class__.get_setting_default(self.key, plugin=self.plugin)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
return self.__class__.get_setting_description(self.key, plugin=self.plugin)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def units(self):
|
|
||||||
return self.__class__.get_setting_units(self.key, plugin=self.plugin)
|
|
||||||
|
|
||||||
def choices(self):
|
|
||||||
return self.__class__.get_setting_choices(self.key, plugin=self.plugin)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_definition(cls, key, **kwargs):
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
@ -182,3 +149,40 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
|||||||
verbose_name=_('Plugin'),
|
verbose_name=_('Plugin'),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
||||||
|
"""
|
||||||
|
This model represents notification settings for a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [
|
||||||
|
('method', 'user', 'key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
REFERENCE_NAME = 'method'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
|
from common.notifications import storage
|
||||||
|
|
||||||
|
kwargs['settings'] = storage.user_settings
|
||||||
|
|
||||||
|
return super().get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
|
method = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_('Method'),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('User'),
|
||||||
|
help_text=_('User'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.key} (for {self.user}): {self.value}'
|
||||||
|
@ -523,7 +523,10 @@ class PluginsRegistry:
|
|||||||
# check all models
|
# check all models
|
||||||
for model in app_config.get_models():
|
for model in app_config.get_models():
|
||||||
# remove model from admin site
|
# remove model from admin site
|
||||||
|
try:
|
||||||
admin.site.unregister(model)
|
admin.site.unregister(model)
|
||||||
|
except: # pragma: no cover
|
||||||
|
pass
|
||||||
models += [model._meta.model_name]
|
models += [model._meta.model_name]
|
||||||
except LookupError: # pragma: no cover
|
except LookupError: # pragma: no cover
|
||||||
# if an error occurs the app was never loaded right -> so nothing to do anymore
|
# if an error occurs the app was never loaded right -> so nothing to do anymore
|
||||||
|
@ -15,8 +15,8 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from plugin.models import PluginConfig, PluginSetting
|
from plugin.models import PluginConfig, PluginSetting, NotificationUserSetting
|
||||||
from common.serializers import SettingsSerializer
|
from common.serializers import GenericReferencedSettingSerializer
|
||||||
|
|
||||||
|
|
||||||
class PluginConfigSerializer(serializers.ModelSerializer):
|
class PluginConfigSerializer(serializers.ModelSerializer):
|
||||||
@ -128,22 +128,25 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class PluginSettingSerializer(SettingsSerializer):
|
class PluginSettingSerializer(GenericReferencedSettingSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for the PluginSetting model
|
Serializer for the PluginSetting model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plugin = serializers.PrimaryKeyRelatedField(read_only=True)
|
MODEL = PluginSetting
|
||||||
|
EXTRA_FIELDS = [
|
||||||
class Meta:
|
|
||||||
model = PluginSetting
|
|
||||||
fields = [
|
|
||||||
'pk',
|
|
||||||
'key',
|
|
||||||
'value',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'type',
|
|
||||||
'choices',
|
|
||||||
'plugin',
|
'plugin',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
plugin = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationUserSettingSerializer(GenericReferencedSettingSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for the PluginSetting model
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODEL = NotificationUserSetting
|
||||||
|
EXTRA_FIELDS = ['method', ]
|
||||||
|
|
||||||
|
method = serializers.CharField(read_only=True)
|
||||||
|
@ -8,7 +8,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
from common.notifications import storage
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@ -73,3 +73,11 @@ def plugin_errors(*args, **kwargs):
|
|||||||
All plugin errors in the current session
|
All plugin errors in the current session
|
||||||
"""
|
"""
|
||||||
return registry.errors
|
return registry.errors
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def notification_settings_list(context, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
List of all user notification settings
|
||||||
|
"""
|
||||||
|
return storage.get_usersettings(user=context.get('user', None))
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
{% setting_object key plugin=plugin as setting %}
|
{% setting_object key plugin=plugin as setting %}
|
||||||
{% elif user_setting %}
|
{% elif user_setting %}
|
||||||
{% setting_object key user=request.user as setting %}
|
{% setting_object key user=request.user as setting %}
|
||||||
|
{% elif notification_setting %}
|
||||||
|
{% setting_object key method=method user=request.user as setting %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% setting_object key as setting %}
|
{% setting_object key as setting %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -22,7 +24,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if setting.is_bool %}
|
{% if setting.is_bool %}
|
||||||
<div class='form-check form-switch'>
|
<div class='form-check form-switch'>
|
||||||
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %} {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %} {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}{% if notification_setting %}notification='{{request.user.id}}'{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id='setting-{{ setting.pk }}'>
|
<div id='setting-{{ setting.pk }}'>
|
||||||
|
@ -70,6 +70,7 @@ $('table').find('.boolean-setting').change(function() {
|
|||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
var plugin = $(this).attr('plugin');
|
var plugin = $(this).attr('plugin');
|
||||||
var user = $(this).attr('user');
|
var user = $(this).attr('user');
|
||||||
|
var notification = $(this).attr('notification');
|
||||||
|
|
||||||
var checked = this.checked;
|
var checked = this.checked;
|
||||||
|
|
||||||
@ -80,6 +81,8 @@ $('table').find('.boolean-setting').change(function() {
|
|||||||
url = `/api/plugin/settings/${pk}/`;
|
url = `/api/plugin/settings/${pk}/`;
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
url = `/api/settings/user/${pk}/`;
|
url = `/api/settings/user/${pk}/`;
|
||||||
|
} else if (notification) {
|
||||||
|
url = `/api/settings/notification/${pk}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
inventreePut(
|
inventreePut(
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
{% load plugin_extras %}
|
||||||
|
|
||||||
{% block label %}user-notifications{% endblock label %}
|
{% block label %}user-notifications{% endblock label %}
|
||||||
|
|
||||||
@ -12,7 +13,10 @@
|
|||||||
<div class='row'>
|
<div class='row'>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="NOTIFICATION_SEND_EMAILS" icon='fa-envelope' user_setting=True %}
|
{% notification_settings_list as settings %}
|
||||||
|
{% for setting in settings %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key=setting.key icon=setting.icon method=setting.method notification_setting=True %}
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +78,7 @@ class RuleSet(models.Model):
|
|||||||
'otp_static_staticdevice',
|
'otp_static_staticdevice',
|
||||||
'plugin_pluginconfig',
|
'plugin_pluginconfig',
|
||||||
'plugin_pluginsetting',
|
'plugin_pluginsetting',
|
||||||
|
'plugin_notificationusersetting',
|
||||||
],
|
],
|
||||||
'part_category': [
|
'part_category': [
|
||||||
'part_partcategory',
|
'part_partcategory',
|
||||||
|
Loading…
Reference in New Issue
Block a user