diff --git a/src/backend/InvenTree/common/notifications.py b/src/backend/InvenTree/common/notifications.py index fe911f1140..dac6f07759 100644 --- a/src/backend/InvenTree/common/notifications.py +++ b/src/backend/InvenTree/common/notifications.py @@ -366,6 +366,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): target_exclude = kwargs.get('target_exclude') context = kwargs.get('context', {}) delivery_methods = kwargs.get('delivery_methods') + check_recent = kwargs.get('check_recent', True) # Check if data is importing currently if isImportingData() or isRebuildingData(): @@ -391,7 +392,9 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): # Check if we have notified recently... delta = timedelta(days=1) - if common.models.NotificationEntry.check_recent(category, obj_ref_value, delta): + if check_recent and common.models.NotificationEntry.check_recent( + category, obj_ref_value, delta + ): logger.info( "Notification '%s' has recently been sent for '%s' - SKIPPING", category, diff --git a/src/backend/InvenTree/plugin/builtin/integration/part_notifications.py b/src/backend/InvenTree/plugin/builtin/integration/part_notifications.py new file mode 100644 index 0000000000..83fd1d31a2 --- /dev/null +++ b/src/backend/InvenTree/plugin/builtin/integration/part_notifications.py @@ -0,0 +1,69 @@ +"""Core set of Notifications as a Plugin.""" + +from django.utils.translation import gettext_lazy as _ + +import structlog + +import common.models +import common.notifications +import InvenTree.helpers +import InvenTree.helpers_email +import InvenTree.helpers_model +import InvenTree.tasks +from part.models import Part +from plugin import InvenTreePlugin +from plugin.mixins import EventMixin, SettingsMixin + +logger = structlog.get_logger('inventree') + + +class PartNotificationsPlugin(SettingsMixin, EventMixin, InvenTreePlugin): + """Core notification methods for InvenTree.""" + + NAME = 'PartNotificationsPlugin' + TITLE = _('Part Notifications') + AUTHOR = _('InvenTree contributors') + DESCRIPTION = _('Notify users about part changes') + VERSION = '1.0.0' + + SETTINGS = { + 'ENABLE_PART_NOTIFICATIONS': { + 'name': _('Send notifications'), + 'description': _('Send notifications for part changes to subscribed users'), + 'default': False, + 'validator': bool, + } + } + + def wants_process_event(self, event): + """Return whether given event should be processed or not.""" + return event.startswith('part_part.') + + def process_event(self, event, *args, **kwargs): + """Custom event processing.""" + if not self.get_setting('ENABLE_PART_NOTIFICATIONS'): + return + part = Part.objects.get(pk=kwargs['id']) + part_action = event.split('.')[-1] + + name = _('Changed part notification') + common.notifications.trigger_notification( + part, + 'part.notification', + target_fnc=part.get_subscribers, + check_recent=False, + context={ + 'part': part, + 'name': name, + 'message': _( + f'The part `{part.name}` has been triggered with a `{part_action}` event' + ), + 'link': InvenTree.helpers_model.construct_absolute_url( + part.get_absolute_url() + ), + 'template': { + 'html': 'email/part_event_notification.html', + 'subject': name, + }, + }, + ) diff --git a/src/backend/InvenTree/templates/email/part_event_notification.html b/src/backend/InvenTree/templates/email/part_event_notification.html new file mode 100644 index 0000000000..c61d2afa92 --- /dev/null +++ b/src/backend/InvenTree/templates/email/part_event_notification.html @@ -0,0 +1,33 @@ +{% extends "email/email.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block title %} +{{ message }} +{% if link %} +
{% trans "Click on the following link to view this part" %}: {{ link }}
+{% endif %} +{% endblock title %} + +{% block body %} +{% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part or a category that it is part of {% endblocktrans %}.
+{% endblock footer_prefix %} diff --git a/src/frontend/src/components/buttons/StarredToggleButton.tsx b/src/frontend/src/components/buttons/StarredToggleButton.tsx new file mode 100644 index 0000000000..6ebc9aaa05 --- /dev/null +++ b/src/frontend/src/components/buttons/StarredToggleButton.tsx @@ -0,0 +1,62 @@ +import { t } from '@lingui/macro'; +import { showNotification } from '@mantine/notifications'; +import { IconBell } from '@tabler/icons-react'; +import { useApi } from '../../contexts/ApiContext'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { apiUrl } from '../../states/ApiState'; +import { ActionButton } from './ActionButton'; + +export default function StarredToggleButton({ + instance, + model, + successFunction +}: Readonly<{ + instance: any; + model: ModelType.part | ModelType.partcategory; + successFunction: () => void; +}>): JSX.Element { + const api = useApi(); + + function change(starred: boolean, partPk: number) { + api + .patch( + apiUrl( + model == ModelType.part + ? ApiEndpoints.part_list + : ApiEndpoints.category_list, + partPk + ), + { starred: !starred } + ) + .then(() => { + showNotification({ + title: 'Subscription updated', + message: `Subscription ${starred ? 'removed' : 'added'}`, + autoClose: 5000, + color: 'blue' + }); + successFunction(); + }) + .catch((error) => { + showNotification({ + title: 'Error', + message: error.message, + autoClose: 5000, + color: 'red' + }); + }); + } + + return ( +