From 6b53fd2bd4c673478941466032ff3f0e03c595ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 02:22:39 +0100 Subject: [PATCH 001/157] Base html [FR] Notification centre Fixes #2279 --- InvenTree/InvenTree/urls.py | 9 ++ InvenTree/InvenTree/views.py | 15 +++ .../InvenTree/notifications/history.html | 25 ++++ .../InvenTree/notifications/inbox.html | 25 ++++ .../notifications/notifications.html | 119 ++++++++++++++++++ .../InvenTree/notifications/sidebar.html | 11 ++ 6 files changed, 204 insertions(+) create mode 100644 InvenTree/templates/InvenTree/notifications/history.html create mode 100644 InvenTree/templates/InvenTree/notifications/inbox.html create mode 100644 InvenTree/templates/InvenTree/notifications/notifications.html create mode 100644 InvenTree/templates/InvenTree/notifications/sidebar.html diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 584403fc84..c6bdf9dfcc 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -41,6 +41,7 @@ from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, from .views import CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView +from .views import NotificationsView from .api import InfoView, NotFoundView from .api import ActionPluginView @@ -87,6 +88,12 @@ settings_urls = [ url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'), ] +notifications_urls = [ + + # Catch any other urls + url(r'^.*$', NotificationsView.as_view(), name='notifications'), +] + # These javascript files are served "dynamically" - i.e. rendered on demand dynamic_javascript_urls = [ url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'), @@ -138,6 +145,8 @@ urlpatterns = [ url(r'^settings/', include(settings_urls)), + url(r'^notifications/', include(notifications_urls)), + url(r'^edit-user/', EditUserView.as_view(), name='edit-user'), url(r'^set-password/', SetPasswordView.as_view(), name='set-password'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 989fb1bc9d..6eb9fb7462 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -884,3 +884,18 @@ class DatabaseStatsView(AjaxView): """ return ctx + + +class NotificationsView(TemplateView): + """ View for showing notifications + """ + + template_name = "InvenTree/notifications/notifications.html" + + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs).copy() + + # ctx['settings'] = InvenTreeSetting.objects.all().order_by('key') + + return ctx diff --git a/InvenTree/templates/InvenTree/notifications/history.html b/InvenTree/templates/InvenTree/notifications/history.html new file mode 100644 index 0000000000..a3c4ffdc0e --- /dev/null +++ b/InvenTree/templates/InvenTree/notifications/history.html @@ -0,0 +1,25 @@ +{% extends "panel.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block label %}history{% endblock %} + +{% block heading %} +{% trans "History" %} +{% endblock %} + +{% block actions %} +
+ {% trans "Refresh History" %} +
+{% endblock %} + +{% block content %} + +
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/notifications/inbox.html b/InvenTree/templates/InvenTree/notifications/inbox.html new file mode 100644 index 0000000000..92c4a56339 --- /dev/null +++ b/InvenTree/templates/InvenTree/notifications/inbox.html @@ -0,0 +1,25 @@ +{% extends "panel.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block label %}inbox{% endblock %} + +{% block heading %} +{% trans "Inbox" %} +{% endblock %} + +{% block actions %} +
+ {% trans "Refresh Inbox" %} +
+{% endblock %} + +{% block content %} + +
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html new file mode 100644 index 0000000000..d10c1dcf7b --- /dev/null +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -0,0 +1,119 @@ +{% extends "base.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block breadcrumb_list %} +{% endblock %} + +{% block page_title %} +{% inventree_title %} | {% trans "Notifications" %} +{% endblock %} + +{% block sidebar %} + {% include "InvenTree/notifications/sidebar.html" %} +{% endblock %} + +{% block content %} + {% include "InvenTree/notifications/inbox.html" %} + {% include "InvenTree/notifications/history.html" %} +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +$("#inbox-table").inventreeTable({ + url: "{% url 'api-part-parameter-template-list' %}", + queryParams: { + ordering: 'name', + }, + formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; }, + columns: [ + { + field: 'pk', + title: '{% trans "ID" %}', + visible: false, + switchable: false, + }, + { + field: 'name', + title: '{% trans "Name" %}', + sortable: 'true', + }, + { + field: 'units', + title: '{% trans "Units" %}', + sortable: 'true', + }, + { + formatter: function(value, row, index, field) { + var bEdit = ""; + var bDel = ""; + + var html = "
" + bEdit + bDel + "
"; + + return html; + } + } + ] +}); + +$("#inbox-table").on('click', '.template-edit', function() { + var button = $(this); + + var url = "/part/parameter/template/" + button.attr('pk') + "/edit/"; + + launchModalForm(url, { + success: function() { + $("#inbox-table").bootstrapTable('refresh'); + } + }); +}); + +$("#inbox-refresh").on('click', function() { + $("#inbox-table").bootstrapTable('refresh'); +}); + + +$("#history-table").inventreeTable({ + url: "{% url 'api-part-parameter-template-list' %}", + queryParams: { + ordering: 'name', + }, + formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; }, + columns: [ + { + field: 'pk', + title: '{% trans "ID" %}', + visible: false, + switchable: false, + }, + { + field: 'name', + title: '{% trans "Name" %}', + sortable: 'true', + }, + { + field: 'units', + title: '{% trans "Units" %}', + sortable: 'true', + }, + { + formatter: function(value, row, index, field) { + var bEdit = ""; + var bDel = ""; + + var html = "
" + bEdit + bDel + "
"; + + return html; + } + } + ] +}); + +$("#history-refresh").on('click', function() { + $("#history-table").bootstrapTable('refresh'); +}); + +enableSidebar('notifications'); +{% endblock %} diff --git a/InvenTree/templates/InvenTree/notifications/sidebar.html b/InvenTree/templates/InvenTree/notifications/sidebar.html new file mode 100644 index 0000000000..aec7af1983 --- /dev/null +++ b/InvenTree/templates/InvenTree/notifications/sidebar.html @@ -0,0 +1,11 @@ +{% load i18n %} +{% load static %} +{% load inventree_extras %} + +{% trans "Notifications" as text %} +{% include "sidebar_header.html" with text=text icon='fa-user' %} + +{% trans "Inbox" as text %} +{% include "sidebar_item.html" with label='inbox' text=text icon="fa-cog" %} +{% trans "History" as text %} +{% include "sidebar_item.html" with label='history' text=text icon="fa-desktop" %} From dd44eb389f152e261023453fd41de926c27b53a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 16:02:23 +0100 Subject: [PATCH 002/157] add a notification message model part of #2282 --- InvenTree/common/admin.py | 10 ++ InvenTree/common/api.py | 59 ++++++- .../migrations/0013_notificationmessage.py | 33 ++++ InvenTree/common/models.py | 91 +++++++++++ InvenTree/common/serializers.py | 38 ++++- .../notifications/notifications.html | 144 ++++++++---------- 6 files changed, 296 insertions(+), 79 deletions(-) create mode 100644 InvenTree/common/migrations/0013_notificationmessage.py diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index c5950a0c0a..627dd0be83 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -43,6 +43,16 @@ class NotificationEntryAdmin(admin.ModelAdmin): list_display = ('key', 'uid', 'updated', ) +class NotificationMessageAdmin(admin.ModelAdmin): + + list_display = ('age_human', 'user', 'category', 'name', 'read', 'target_object', 'source_object', ) + + list_filter = ('category', 'read', 'user', ) + + search_fields = ('name', 'category', 'message', ) + + admin.site.register(common.models.InvenTreeSetting, SettingsAdmin) admin.site.register(common.models.InvenTreeUserSetting, UserSettingsAdmin) admin.site.register(common.models.NotificationEntry, NotificationEntryAdmin) +admin.site.register(common.models.NotificationMessage, NotificationMessageAdmin) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 4ec6bf9441..df9cb6161c 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -130,6 +130,57 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView): ] +class NotificationList(generics.ListAPIView): + queryset = common.models.NotificationMessage.objects.all() + serializer_class = common.serializers.NotificationMessageSerializer + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + ordering_fields = [ + #'age', # TODO enable ordering by age + 'category', + 'name', + ] + + search_fields = [ + 'name', + 'message', + ] + + def filter_queryset(self, queryset): + """ + Only list notifications which apply to the current user + """ + + try: + user = self.request.user + except AttributeError: + return common.models.NotificationMessage.objects.none() + + queryset = super().filter_queryset(queryset) + queryset = queryset.filter(user=user) + return queryset + + +class NotificationDetail(generics.RetrieveDestroyAPIView): + """ + Detail view for an individual notification object + + - User can only view / delete their own notification objects + """ + + queryset = common.models.NotificationMessage.objects.all() + serializer_class = common.serializers.NotificationMessageSerializer + + permission_classes = [ + UserSettingsPermissions, + ] + + common_api_urls = [ # User settings @@ -148,6 +199,12 @@ common_api_urls = [ # Global Settings List url(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), - ])) + ])), + + # Notifications + url(r'^notifications/', include([ + url(r'^(?P\d+)/', NotificationDetail.as_view(), name='api-notifications-detail'), + url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'), + ])), ] diff --git a/InvenTree/common/migrations/0013_notificationmessage.py b/InvenTree/common/migrations/0013_notificationmessage.py new file mode 100644 index 0000000000..eee0584848 --- /dev/null +++ b/InvenTree/common/migrations/0013_notificationmessage.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.5 on 2021-11-27 14:51 + +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), + ('contenttypes', '0002_remove_content_type_name'), + ('common', '0012_notificationentry'), + ] + + operations = [ + migrations.CreateModel( + name='NotificationMessage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('target_object_id', models.PositiveIntegerField()), + ('source_object_id', models.PositiveIntegerField(blank=True, null=True)), + ('category', models.CharField(max_length=250)), + ('name', models.CharField(max_length=250)), + ('message', models.CharField(blank=True, max_length=250, null=True)), + ('creation', models.DateTimeField(auto_now=True)), + ('read', models.BooleanField(default=False)), + ('source_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notification_source', to='contenttypes.contenttype')), + ('target_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_target', to='contenttypes.contenttype')), + ('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')), + ], + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 4028d352a0..96f3437ae6 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -13,8 +13,13 @@ from datetime import datetime, timedelta from django.db import models, transaction from django.contrib.auth.models import User, Group +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db.utils import IntegrityError, OperationalError from django.conf import settings +from django.urls import reverse +from django.utils.timezone import now +from django.contrib.humanize.templatetags.humanize import naturaltime from djmoney.settings import CURRENCY_CHOICES from djmoney.contrib.exchange.models import convert_money @@ -1419,3 +1424,89 @@ class NotificationEntry(models.Model): ) entry.save() + + +class NotificationMessage(models.Model): + """ + A NotificationEntry records the last time a particular notifaction was sent out. + + It is recorded to ensure that notifications are not sent out "too often" to users. + + Attributes: + - key: A text entry describing the notification e.g. 'part.notify_low_stock' + - uid: An (optional) numerical ID for a particular instance + - date: The last time this notification was sent + """ + + # generic link to target + target_content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + related_name='notification_target', + ) + + target_object_id = models.PositiveIntegerField() + + target_object = GenericForeignKey('target_content_type', 'target_object_id') + + # generic link to source + source_content_type = models.ForeignKey( + ContentType, + on_delete=models.SET_NULL, + related_name='notification_source', + null=True, + blank=True, + ) + + source_object_id = models.PositiveIntegerField( + null=True, + blank=True, + ) + + source_object = GenericForeignKey('source_content_type', 'source_object_id') + + # user that receives the notification + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + verbose_name=_('User'), + help_text=_('User'), + ) + + category = models.CharField( + max_length=250, + blank=False, + ) + + name = models.CharField( + max_length=250, + blank=False, + ) + + message = models.CharField( + max_length=250, + blank=True, + null=True, + ) + + creation = models.DateTimeField( + auto_now=True, + null=False, + ) + + read = models.BooleanField( + default=False, + ) + + @staticmethod + def get_api_url(): + return reverse('api-notifications-list') + + def age(self): + """age of the message in seconds""" + delta = now() - self.creation + return delta.seconds + + def age_human(self): + """humanized age""" + return naturaltime(self.creation) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 60eb609dc1..958802755e 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -9,7 +9,7 @@ from InvenTree.serializers import InvenTreeModelSerializer from rest_framework import serializers -from common.models import InvenTreeSetting, InvenTreeUserSetting +from common.models import InvenTreeSetting, InvenTreeUserSetting, NotificationMessage class SettingsSerializer(InvenTreeModelSerializer): @@ -95,3 +95,39 @@ class UserSettingsSerializer(SettingsSerializer): 'type', 'choices', ] + + +class NotificationMessageSerializer(SettingsSerializer): + """ + Serializer for the InvenTreeUserSetting model + """ + + #content_object = serializers.PrimaryKeyRelatedField(read_only=True) + + user = serializers.PrimaryKeyRelatedField(read_only=True) + + category = serializers.CharField(read_only=True) + + name = serializers.CharField(read_only=True) + + message = serializers.CharField(read_only=True) + + creation = serializers.CharField(read_only=True) + + age = serializers.IntegerField() + + age_human = serializers.CharField() + + class Meta: + model = NotificationMessage + fields = [ + 'pk', + #'content_object', + 'user', + 'category', + 'name', + 'message', + 'creation', + 'age', + 'age_human', + ] diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index d10c1dcf7b..e3d30117c6 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -22,52 +22,72 @@ {% block js_ready %} {{ block.super }} -$("#inbox-table").inventreeTable({ - url: "{% url 'api-part-parameter-template-list' %}", - queryParams: { - ordering: 'name', - }, - formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; }, - columns: [ - { - field: 'pk', - title: '{% trans "ID" %}', - visible: false, - switchable: false, - }, - { - field: 'name', - title: '{% trans "Name" %}', - sortable: 'true', - }, - { - field: 'units', - title: '{% trans "Units" %}', - sortable: 'true', - }, - { - formatter: function(value, row, index, field) { - var bEdit = ""; - var bDel = ""; +function loadNotificationTable(table, options={}) { - var html = "
" + bEdit + bDel + "
"; - - return html; + $(table).inventreeTable({ + url: options.url, + name: options.name, + groupBy: false, + search: true, + queryParams: { + ordering: 'age', + }, + paginationVAlign: 'bottom', + original: options.params, + formatNoMatches: options.no_matches, + columns: [ + { + field: 'pk', + title: '{% trans "ID" %}', + visible: false, + switchable: false, + }, + { + field: 'age', + title: '{% trans "Age" %}', + sortable: 'true', + formatter: function(value, row) { + return row.age_human + } + }, + { + field: 'category', + title: '{% trans "Category" %}', + sortable: 'true', + }, + { + field: 'name', + title: '{% trans "Name" %}', + }, + { + field: 'message', + title: '{% trans "Message" %}', + }, + { + formatter: function(value, row, index, field) { + var bRead = ""; + var html = "
" + bRead + "
"; + return html; + } } - } - ] -}); - -$("#inbox-table").on('click', '.template-edit', function() { - var button = $(this); - - var url = "/part/parameter/template/" + button.attr('pk') + "/edit/"; - - launchModalForm(url, { - success: function() { - $("#inbox-table").bootstrapTable('refresh'); - } + ] }); + + $(table).on('click', '.notification-read', function() { + var url = "/notifications/" + $(this).attr('pk') + "/"; + + inventreeDelete(url, { + success: function() { + $(table).bootstrapTable('refresh'); + } + }); + }); +} + +loadNotificationTable("#inbox-table", { + name: 'inbox', + url: '{% url 'api-notifications-list' %}', + no_matches: function() { return '{% trans "No unread notifications found" %}'; }, }); $("#inbox-refresh").on('click', function() { @@ -75,40 +95,10 @@ $("#inbox-refresh").on('click', function() { }); -$("#history-table").inventreeTable({ - url: "{% url 'api-part-parameter-template-list' %}", - queryParams: { - ordering: 'name', - }, - formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; }, - columns: [ - { - field: 'pk', - title: '{% trans "ID" %}', - visible: false, - switchable: false, - }, - { - field: 'name', - title: '{% trans "Name" %}', - sortable: 'true', - }, - { - field: 'units', - title: '{% trans "Units" %}', - sortable: 'true', - }, - { - formatter: function(value, row, index, field) { - var bEdit = ""; - var bDel = ""; - - var html = "
" + bEdit + bDel + "
"; - - return html; - } - } - ] +loadNotificationTable("#history-table", { + name: 'history', + url: '{% url 'api-notifications-list' %}', + no_matches: function() { return '{% trans "No notification history found" %}'; }, }); $("#history-refresh").on('click', function() { From a9995087ac971231019b2ad2e1aea60890ad0382 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 16:21:17 +0100 Subject: [PATCH 003/157] move spacing from id to class --- InvenTree/templates/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index c339d7b4e1..f4cb245777 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -71,7 +71,7 @@ {% endif %} - {% endif %} + + +
  • '; + // d-flex justify-content-between align-items-start + html += '
    '; + html += `${item.category}${item.name}`; + html += '
    '; + if (item.target) { + var link_text = `${item.target.model}: ${item.target.name}`; + if (item.target.link) { + link_text = `${link_text}`; + } + html += link_text + } + html += '
    '; + html += `${item.age_human}`; + html += "
  • "; + }); + + // package up + html = `
      ${html}
    ` + } + + // set html + $('#notification-center').html(html); + } + } + ); +} + + +function closeNotificationPanel() { + $('#notification-center').html(`

    {% trans "Notifications will load here" %}

    `); +} From 539910c5946cc464a2ecb6881fceb2cb80a02be2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 02:39:57 +0100 Subject: [PATCH 019/157] translate mesages --- InvenTree/templates/notifications.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/notifications.html b/InvenTree/templates/notifications.html index 16e11f194d..bec96d1585 100644 --- a/InvenTree/templates/notifications.html +++ b/InvenTree/templates/notifications.html @@ -6,7 +6,7 @@
    -

    Notifications will load here

    +

    {% trans "Notifications will load here" %}


    {% trans "Show all notifications and history" %} From 41258415435eb4ed95dc2546ba39887b0dd39dc1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 02:44:05 +0100 Subject: [PATCH 020/157] fix margins --- InvenTree/InvenTree/static/script/inventree/notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index a2ef25f6ab..7195b4d9d6 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -164,7 +164,7 @@ function openNotificationPanel() { html += '
  • '; // d-flex justify-content-between align-items-start html += '
    '; - html += `${item.category}${item.name}`; + html += `${item.category}${item.name}`; html += '
    '; if (item.target) { var link_text = `${item.target.model}: ${item.target.name}`; From 90b3f638516e794ac451650906977a454b987e6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 16:47:40 +0100 Subject: [PATCH 021/157] add ruleset --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 4d1b46ae5d..b9dffa39c6 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -152,6 +152,7 @@ class RuleSet(models.Model): 'common_inventreesetting', 'common_inventreeusersetting', 'common_notificationentry', + 'common_notificationmessage', 'company_contact', 'users_owner', From 2747d0d609980f2a0b52fdc5a77e3da4bdf996ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 16:52:19 +0100 Subject: [PATCH 022/157] PEP fix --- InvenTree/InvenTree/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index f5b03fe9b4..6765180e1a 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -700,7 +700,7 @@ def clean_decimal(number): def get_objectreference(obj, type_ref: str = 'content_type', object_ref: str = 'object_id'): """lookup method for the GenericForeignKey fields - + Attributes: - obj: object that will be resolved - type_ref: field name for the contenttype field in the model From f9655f5eacfad54b1db22e9499683f53f77ea3bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 00:17:00 +0100 Subject: [PATCH 023/157] fix base message serializer --- InvenTree/common/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index e4fdf46c4c..b1880c4339 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -98,14 +98,14 @@ class UserSettingsSerializer(SettingsSerializer): ] -class NotificationMessageSerializer(SettingsSerializer): +class NotificationMessageSerializer(InvenTreeModelSerializer): """ Serializer for the InvenTreeUserSetting model """ - target = serializers.SerializerMethodField() + target = serializers.SerializerMethodField(read_only=True) - source = serializers.SerializerMethodField() + source = serializers.SerializerMethodField(read_only=True) user = serializers.PrimaryKeyRelatedField(read_only=True) @@ -117,11 +117,11 @@ class NotificationMessageSerializer(SettingsSerializer): creation = serializers.CharField(read_only=True) - age = serializers.IntegerField() + age = serializers.IntegerField(read_only=True) - age_human = serializers.CharField() + age_human = serializers.CharField(read_only=True) - read = serializers.BooleanField() + read = serializers.BooleanField(read_only=True) def get_target(self, obj): return get_objectreference(obj, 'target_content_type', 'target_object_id') From 06f6587050b320867fe8eac2c181b1455e489873 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 01:08:06 +0100 Subject: [PATCH 024/157] add endpoint + buttons to mark a notification read --- InvenTree/common/api.py | 36 ++++++++++++++++++- InvenTree/common/serializers.py | 8 +++++ .../notifications/notifications.html | 5 +-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 8d8c9e244b..2f468702a2 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -9,6 +9,7 @@ from django.conf.urls import url, include from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters, generics, permissions +from rest_framework import serializers import common.models import common.serializers @@ -186,6 +187,33 @@ class NotificationDetail(generics.RetrieveDestroyAPIView): ] +class NotificationRead(generics.CreateAPIView): + """ + API endpoint to mark a notification as read. + """ + + queryset = common.models.NotificationMessage.objects.all() + serializer_class = common.serializers.NotificationReadSerializer + + permission_classes = [ + UserSettingsPermissions, + ] + + def get_serializer_context(self): + context = super().get_serializer_context() + if self.request: + context['instance'] = self.get_object() + return context + + def perform_create(self, serializer): + message = self.get_object() + try: + message.read = True + message.save() + except Exception as exc: + raise serializers.ValidationError(detail=serializers.as_serializer_error(exc)) + + settings_api_urls = [ # User settings url(r'^user/', include([ @@ -210,7 +238,13 @@ common_api_urls = [ # Notifications url(r'^notifications/', include([ - url(r'^(?P\d+)/', NotificationDetail.as_view(), name='api-notifications-detail'), + # Individual purchase order detail URLs + url(r'^(?P\d+)/', include([ + url(r'^read/', NotificationRead.as_view(), name='api-notifications-read'), + url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'), + ])), + + # Notification messages list url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'), ])), diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index b1880c4339..71ccac8a4d 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -144,3 +144,11 @@ class NotificationMessageSerializer(InvenTreeModelSerializer): 'age_human', 'read', ] + + +class NotificationReadSerializer(NotificationMessageSerializer): + + def is_valid(self, raise_exception=False): + self.instance = self.context['instance'] # set instance that should be returned + self._validated_data = True + return True diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index e412eef922..cd48584d71 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -91,9 +91,10 @@ function loadNotificationTable(table, options={}) { }); $(table).on('click', '.notification-read', function() { - var url = "/notifications/" + $(this).attr('pk') + "/"; + var url = "/api/notifications/" + $(this).attr('pk') + "/read/"; - inventreeDelete(url, { + inventreePut(url, {}, { + method: 'POST', success: function() { $(table).bootstrapTable('refresh'); } From d57ebde265f29af19c821df1c11da8cb86acc645 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 01:08:54 +0100 Subject: [PATCH 025/157] also add a unread endpoint --- InvenTree/common/api.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 2f468702a2..904567f75c 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -187,9 +187,9 @@ class NotificationDetail(generics.RetrieveDestroyAPIView): ] -class NotificationRead(generics.CreateAPIView): +class NotificationReadEdit(generics.CreateAPIView): """ - API endpoint to mark a notification as read. + general API endpoint to manipulate read state of a notification """ queryset = common.models.NotificationMessage.objects.all() @@ -208,12 +208,26 @@ class NotificationRead(generics.CreateAPIView): def perform_create(self, serializer): message = self.get_object() try: - message.read = True + message.read = self.target message.save() except Exception as exc: raise serializers.ValidationError(detail=serializers.as_serializer_error(exc)) +class NotificationRead(NotificationReadEdit): + """ + API endpoint to mark a notification as read. + """ + target = True + + +class NotificationUnread(NotificationReadEdit): + """ + API endpoint to mark a notification as unread. + """ + target = False + + settings_api_urls = [ # User settings url(r'^user/', include([ @@ -241,6 +255,7 @@ common_api_urls = [ # Individual purchase order detail URLs url(r'^(?P\d+)/', include([ url(r'^read/', NotificationRead.as_view(), name='api-notifications-read'), + url(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'), url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'), ])), From 5bfe8912085072a8b513ab140d1a8c63e4a1cfbf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 01:09:36 +0100 Subject: [PATCH 026/157] add button for unread + refactor --- .../notifications/notifications.html | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index cd48584d71..14d9fc49ab 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -22,6 +22,11 @@ {% block js_ready %} {{ block.super }} +function updateNotificationTables() { + $("#inbox-table").bootstrapTable('refresh'); + $("#history-table").bootstrapTable('refresh'); +} + function loadNotificationTable(table, options={}) { var params = options.params || {}; @@ -82,7 +87,16 @@ function loadNotificationTable(table, options={}) { }, { formatter: function(value, row, index, field) { - var bRead = ""; + if (row.read) { + var bReadText = '{% trans "Mark as unread" %}'; + var bReadIcon = 'fa-uncheck icon-red'; + var bReadTarget = 'unread'; + } else { + var bReadText = '{% trans "Mark as read" %}'; + var bReadIcon = 'fa-check icon-green'; + var bReadTarget = 'read'; + } + var bRead = ``; var html = "
    " + bRead + "
    "; return html; } @@ -91,13 +105,11 @@ function loadNotificationTable(table, options={}) { }); $(table).on('click', '.notification-read', function() { - var url = "/api/notifications/" + $(this).attr('pk') + "/read/"; + var url = `/api/notifications/${$(this).attr('pk')}/${$(this).attr('target')}/`; inventreePut(url, {}, { method: 'POST', - success: function() { - $(table).bootstrapTable('refresh'); - } + success: function() { updateNotificationTables(); } }); }); } From 07b0577b947146f1f41bb1def1820934c720dfb1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 01:49:35 +0100 Subject: [PATCH 027/157] enable the notification panel to mark messages as read --- .../static/script/inventree/notification.js | 37 ++++++++++++++++++- .../notifications/notifications.html | 13 ++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 7195b4d9d6..1854ead829 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -146,8 +146,26 @@ function showMessage(message, options={}) { } +/** + * Returns the html for a read / unread button + **/ +function getReadEditButton(pk, state) { + if (state) { + var bReadText = '{% trans "Mark as unread" %}'; + var bReadIcon = 'fa-uncheck icon-red'; + var bReadTarget = 'unread'; + } else { + var bReadText = '{% trans "Mark as read" %}'; + var bReadIcon = 'fa-check icon-green'; + var bReadTarget = 'read'; + } + return ``; +} + + function openNotificationPanel() { var html = ''; + var center_ref = '#notification-center'; inventreeGet( '/api/notifications/', @@ -175,6 +193,7 @@ function openNotificationPanel() { } html += '
    '; html += `${item.age_human}`; + html += getReadEditButton(item.pk, item.read); html += "
  • "; }); @@ -183,10 +202,26 @@ function openNotificationPanel() { } // set html - $('#notification-center').html(html); + $(center_ref).html(html); } } ); + + $(center_ref).on('click', '.notification-read', function() { + caller = $(this); + var url = `/api/notifications/${caller.attr('pk')}/${caller.attr('target')}/`; + + inventreePut(url, {}, { + method: 'POST', + success: function() { + // update the notification tables if they exsist + if (window.updateNotifications) { + window.updateNotifications(); + } + caller.parent().parent().remove() + } + }); + }); } diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index 14d9fc49ab..503c492ac3 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -26,6 +26,8 @@ function updateNotificationTables() { $("#inbox-table").bootstrapTable('refresh'); $("#history-table").bootstrapTable('refresh'); } +// this allows the global notification panel to update the tables +window.updateNotifications = updateNotificationTables function loadNotificationTable(table, options={}) { @@ -87,16 +89,7 @@ function loadNotificationTable(table, options={}) { }, { formatter: function(value, row, index, field) { - if (row.read) { - var bReadText = '{% trans "Mark as unread" %}'; - var bReadIcon = 'fa-uncheck icon-red'; - var bReadTarget = 'unread'; - } else { - var bReadText = '{% trans "Mark as read" %}'; - var bReadIcon = 'fa-check icon-green'; - var bReadTarget = 'read'; - } - var bRead = ``; + var bRead = getReadEditButton(row.pk, row.read) var html = "
    " + bRead + "
    "; return html; } From 2dbb21f8e765eadc7d94319b7927266f0291571b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 23:46:41 +0100 Subject: [PATCH 028/157] make creation date none changeable on save --- .../0014_alter_notificationmessage_creation.py | 18 ++++++++++++++++++ InvenTree/common/models.py | 4 +--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 InvenTree/common/migrations/0014_alter_notificationmessage_creation.py diff --git a/InvenTree/common/migrations/0014_alter_notificationmessage_creation.py b/InvenTree/common/migrations/0014_alter_notificationmessage_creation.py new file mode 100644 index 0000000000..710e2c7e32 --- /dev/null +++ b/InvenTree/common/migrations/0014_alter_notificationmessage_creation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-11-29 22:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0013_notificationmessage'), + ] + + operations = [ + migrations.AlterField( + model_name='notificationmessage', + name='creation', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 10b2796768..5ccd9cf7a7 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1492,9 +1492,7 @@ class NotificationMessage(models.Model): ) creation = models.DateTimeField( - auto_now=True, - null=False, - editable=False, + auto_now_add=True, ) read = models.BooleanField( From 0cefbe41c8810cf408a422e91071bcc186904fdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 23:50:15 +0100 Subject: [PATCH 029/157] PEP fix --- InvenTree/common/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 904567f75c..563c8fc79f 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -259,7 +259,7 @@ common_api_urls = [ url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'), ])), - # Notification messages list + # Notification messages list url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'), ])), From 5614fde36806a85cabc9945a7c8de5618a0dd7c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 16:55:39 +0100 Subject: [PATCH 030/157] change out icons --- InvenTree/InvenTree/static/script/inventree/notification.js | 6 +++--- InvenTree/templates/base.html | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 1854ead829..ad46d1385a 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -152,14 +152,14 @@ function showMessage(message, options={}) { function getReadEditButton(pk, state) { if (state) { var bReadText = '{% trans "Mark as unread" %}'; - var bReadIcon = 'fa-uncheck icon-red'; + var bReadIcon = 'fas fa-bookmark icon-red'; var bReadTarget = 'unread'; } else { var bReadText = '{% trans "Mark as read" %}'; - var bReadIcon = 'fa-check icon-green'; + var bReadIcon = 'far fa-bookmark icon-green'; var bReadTarget = 'read'; } - return ``; + return ``; } diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index fb58016607..5bce256647 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -182,6 +182,7 @@ + From 791da9b322b21b11e43f509aadabdb7e8039da28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 16:56:55 +0100 Subject: [PATCH 031/157] add notification counter to indicator --- .../static/script/inventree/notification.js | 15 ++++++++++----- InvenTree/templates/navbar.html | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index ad46d1385a..bb40fa3850 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -134,17 +134,22 @@ function showMessage(message, options={}) { }, { success: function(response) { - if (response.length == 0) { - $("#notification-alert").addClass("d-none"); - } else { - $("#notification-alert").removeClass("d-none"); - } + updateNotificationIndicator(response.length); } } ); } } +function updateNotificationIndicator(counter) { + if (counter == 0) { + $("#notification-alert").addClass("d-none"); + } else { + $("#notification-alert").removeClass("d-none"); + } + $("#notification-counter").html(counter); +} + /** * Returns the html for a read / unread button diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 7f8d17dea1..334c1ebcc7 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -75,8 +75,9 @@ From 753199febb41890b6de945c67f7667d3201ed955 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 16:57:53 +0100 Subject: [PATCH 032/157] refactor --- InvenTree/InvenTree/static/script/inventree/notification.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index bb40fa3850..cca807b81c 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -141,13 +141,13 @@ function showMessage(message, options={}) { } } -function updateNotificationIndicator(counter) { - if (counter == 0) { +function updateNotificationIndicator(count) { + if (count == 0) { $("#notification-alert").addClass("d-none"); } else { $("#notification-alert").removeClass("d-none"); } - $("#notification-counter").html(counter); + $("#notification-counter").html(count); } From c8efc26f58950770c67ee9e251fbb50b7d7564c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 16:57:58 +0100 Subject: [PATCH 033/157] docs --- InvenTree/InvenTree/static/script/inventree/notification.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index cca807b81c..5f9b35c9e2 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -141,6 +141,9 @@ function showMessage(message, options={}) { } } +/** + * updates the notification counter + **/ function updateNotificationIndicator(count) { if (count == 0) { $("#notification-alert").addClass("d-none"); From 93f9fe9684d9f4d0c5025dce02d8d2cc016734b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:00:02 +0100 Subject: [PATCH 034/157] update indicator when table is updated --- .../InvenTree/notifications/notifications.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index 503c492ac3..e6f73db356 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -102,7 +102,19 @@ function loadNotificationTable(table, options={}) { inventreePut(url, {}, { method: 'POST', - success: function() { updateNotificationTables(); } + success: function() { + updateNotificationTables(); + + // update current notification count + var count = parseInt($("#notification-counter").html()); + if ($(this).attr('target') == 'read') { + count = count - 1; + } else { + count = count + 1; + } + // update notification indicator now + updateNotificationIndicator(count); + } }); }); } From d34aa04c7da680b34b81e6b1a1ce4e55273bbbfa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:11:33 +0100 Subject: [PATCH 035/157] refactor to only have one read state updater js codeblock --- .../static/script/inventree/notification.js | 44 +++++++++++++------ .../notifications/notifications.html | 19 +------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 5f9b35c9e2..00ce07afb3 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -216,19 +216,7 @@ function openNotificationPanel() { ); $(center_ref).on('click', '.notification-read', function() { - caller = $(this); - var url = `/api/notifications/${caller.attr('pk')}/${caller.attr('target')}/`; - - inventreePut(url, {}, { - method: 'POST', - success: function() { - // update the notification tables if they exsist - if (window.updateNotifications) { - window.updateNotifications(); - } - caller.parent().parent().remove() - } - }); + updateNotificationReadState($(this), true); }); } @@ -236,3 +224,33 @@ function openNotificationPanel() { function closeNotificationPanel() { $('#notification-center').html(`

    {% trans "Notifications will load here" %}

    `); } + + +function updateNotificationReadState(btn, panel_caller=false) { + var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`; + + inventreePut(url, {}, { + method: 'POST', + success: function() { + // update the notification tables if they exsist + if (window.updateNotifications) { + window.updateNotifications(); + } + + // update current notification count + var count = parseInt($("#notification-counter").html()); + if (btn.attr('target') == 'read') { + count = count - 1; + } else { + count = count + 1; + } + // update notification indicator now + updateNotificationIndicator(count); + + // remove notification if in panel + if (panel_caller) { + btn.parent().parent().remove() + } + } + }); +}; diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index e6f73db356..e6b15a9ff0 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -98,24 +98,7 @@ function loadNotificationTable(table, options={}) { }); $(table).on('click', '.notification-read', function() { - var url = `/api/notifications/${$(this).attr('pk')}/${$(this).attr('target')}/`; - - inventreePut(url, {}, { - method: 'POST', - success: function() { - updateNotificationTables(); - - // update current notification count - var count = parseInt($("#notification-counter").html()); - if ($(this).attr('target') == 'read') { - count = count - 1; - } else { - count = count + 1; - } - // update notification indicator now - updateNotificationIndicator(count); - } - }); + updateNotificationReadState($(this)); }); } From d54231452080d16d998497f2d24712c9e133463f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:14:23 +0100 Subject: [PATCH 036/157] better docs --- InvenTree/InvenTree/static/script/inventree/notification.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 00ce07afb3..847c95dba7 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -232,7 +232,7 @@ function updateNotificationReadState(btn, panel_caller=false) { inventreePut(url, {}, { method: 'POST', success: function() { - // update the notification tables if they exsist + // update the notification tables if they were declared if (window.updateNotifications) { window.updateNotifications(); } @@ -247,7 +247,7 @@ function updateNotificationReadState(btn, panel_caller=false) { // update notification indicator now updateNotificationIndicator(count); - // remove notification if in panel + // remove notification if called from notification panel if (panel_caller) { btn.parent().parent().remove() } From a1e0bef70dadfac085d725c1d8f22bc247763ac5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:19:04 +0100 Subject: [PATCH 037/157] code restructure --- .../static/script/inventree/notification.js | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 847c95dba7..7181cf01c9 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -124,7 +124,7 @@ function showMessage(message, options={}) { * The notification checker is initiated when the document is loaded. It checks if there are unread notifications * if unread messages exist the alert flag is raised by making it visible **/ - function notificationCheck() { +function notificationCheck() { // only refresh state if in focus if (document.hasFocus()) { inventreeGet( @@ -142,17 +142,36 @@ function showMessage(message, options={}) { } /** - * updates the notification counter + * handles read / unread buttons and UI rebuilding **/ -function updateNotificationIndicator(count) { - if (count == 0) { - $("#notification-alert").addClass("d-none"); - } else { - $("#notification-alert").removeClass("d-none"); - } - $("#notification-counter").html(count); -} +function updateNotificationReadState(btn, panel_caller=false) { + var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`; + inventreePut(url, {}, { + method: 'POST', + success: function() { + // update the notification tables if they were declared + if (window.updateNotifications) { + window.updateNotifications(); + } + + // update current notification count + var count = parseInt($("#notification-counter").html()); + if (btn.attr('target') == 'read') { + count = count - 1; + } else { + count = count + 1; + } + // update notification indicator now + updateNotificationIndicator(count); + + // remove notification if called from notification panel + if (panel_caller) { + btn.parent().parent().remove() + } + } + }); +}; /** * Returns the html for a read / unread button @@ -225,32 +244,14 @@ function closeNotificationPanel() { $('#notification-center').html(`

    {% trans "Notifications will load here" %}

    `); } - -function updateNotificationReadState(btn, panel_caller=false) { - var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`; - - inventreePut(url, {}, { - method: 'POST', - success: function() { - // update the notification tables if they were declared - if (window.updateNotifications) { - window.updateNotifications(); - } - - // update current notification count - var count = parseInt($("#notification-counter").html()); - if (btn.attr('target') == 'read') { - count = count - 1; - } else { - count = count + 1; - } - // update notification indicator now - updateNotificationIndicator(count); - - // remove notification if called from notification panel - if (panel_caller) { - btn.parent().parent().remove() - } +/** + * updates the notification counter + **/ +function updateNotificationIndicator(count) { + if (count == 0) { + $("#notification-alert").addClass("d-none"); + } else { + $("#notification-alert").removeClass("d-none"); } - }); -}; + $("#notification-counter").html(count); +} From 5d7a153d873472184f079efe272aca40b0566001 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:22:31 +0100 Subject: [PATCH 038/157] document arguments --- .../InvenTree/static/script/inventree/notification.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 7181cf01c9..8e4b05a546 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -143,6 +143,12 @@ function notificationCheck() { /** * handles read / unread buttons and UI rebuilding + * + * arguments: + * - btn: element that got clicked / fired the event -> must contain pk and target as attributes + * + * options: + * - panel_caller: this button was clicked in the notification panel **/ function updateNotificationReadState(btn, panel_caller=false) { var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`; @@ -175,6 +181,10 @@ function updateNotificationReadState(btn, panel_caller=false) { /** * Returns the html for a read / unread button + * + * arguments: + * - pk: primary key of the notification + * - state: current state of the notification (read / unread) -> just pass what you were handed by the api **/ function getReadEditButton(pk, state) { if (state) { From 1d123827bcb1b719d210785fb1e46096f2e6f22d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:25:15 +0100 Subject: [PATCH 039/157] more doc because more than 3 contributers means we need it --- .../InvenTree/static/script/inventree/notification.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 8e4b05a546..bdee5d200f 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -199,7 +199,9 @@ function getReadEditButton(pk, state) { return ``; } - +/** + * fills the notification panel when opened + **/ function openNotificationPanel() { var html = ''; var center_ref = '#notification-center'; @@ -249,7 +251,9 @@ function openNotificationPanel() { }); } - +/** + * clears the notification panel when closed + **/ function closeNotificationPanel() { $('#notification-center').html(`

    {% trans "Notifications will load here" %}

    `); } From 92c3eaef8f118cc174b1228926058df3291038d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:34:10 +0100 Subject: [PATCH 040/157] reduce api calls to really just check every 5 seconds --- .../static/script/inventree/inventree.js | 7 ++++--- .../static/script/inventree/notification.js | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 1182552bf8..dd9a725fa1 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -218,11 +218,12 @@ function inventreeDocReady() { // Display any cached alert messages showCachedAlerts(); - // Start notification background worker to check every 5 seconds if notifications are available - setInterval(notificationCheck, 5000); + // Start notification background worker to check every 1 seconds if notifications are available + setInterval(notificationCheck, 1000); + // also run when the focus returns $(document).on('focus', function(e){ - notificationCheck(); + notificationCheck(force = true); }); $('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel) // listener for opening the notification panel diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index bdee5d200f..50a260af97 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -119,14 +119,20 @@ function showMessage(message, options={}) { }); } - +var notificationUpdateTic = 0; /** * The notification checker is initiated when the document is loaded. It checks if there are unread notifications * if unread messages exist the alert flag is raised by making it visible **/ -function notificationCheck() { - // only refresh state if in focus - if (document.hasFocus()) { +function notificationCheck(force = false) { + notificationUpdateTic = notificationUpdateTic + 1; + + console.log(notificationUpdateTic); + + // refresh if forced or + // if in focus and was not refreshed in the last 5 seconds + if (force || (document.hasFocus() && notificationUpdateTic >= 5)) { + notificationUpdateTic = 0; inventreeGet( '/api/notifications/', { @@ -262,6 +268,9 @@ function closeNotificationPanel() { * updates the notification counter **/ function updateNotificationIndicator(count) { + // reset update Ticker -> safe some API bandwidth + notificationUpdateTic = 0; + if (count == 0) { $("#notification-alert").addClass("d-none"); } else { From cc9107d72a39ca24b302fc7cad95697d026ffd8b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:35:40 +0100 Subject: [PATCH 041/157] update docs --- InvenTree/InvenTree/static/script/inventree/notification.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 50a260af97..c5020169d6 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -122,7 +122,10 @@ function showMessage(message, options={}) { var notificationUpdateTic = 0; /** * The notification checker is initiated when the document is loaded. It checks if there are unread notifications - * if unread messages exist the alert flag is raised by making it visible + * if unread messages exist the notification indicator is updated + * + * options: + * - force: set true to force an update now (if you got in focus for example) **/ function notificationCheck(force = false) { notificationUpdateTic = notificationUpdateTic + 1; From ec7ef6ef1e415981184088fa98cda04931fa95c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 17:45:56 +0100 Subject: [PATCH 042/157] reduce needed cyle when idleing --- .../static/script/inventree/inventree.js | 12 +++++++----- .../static/script/inventree/notification.js | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index dd9a725fa1..0e7c09c32d 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -218,12 +218,14 @@ function inventreeDocReady() { // Display any cached alert messages showCachedAlerts(); - // Start notification background worker to check every 1 seconds if notifications are available - setInterval(notificationCheck, 1000); + // always refresh when the focus returns + $(document).focus(function(){ + startNotificationWatcher(); + }); - // also run when the focus returns - $(document).on('focus', function(e){ - notificationCheck(force = true); + // kill notification watcher if focus is lost -> respect your users cycles + $(document).focusout(function(){ + stopNotificationWatcher(); }); $('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel) // listener for opening the notification panel diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index c5020169d6..4fc9b0ac77 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -119,6 +119,23 @@ function showMessage(message, options={}) { }); } +var notificationWatcher = null; // reference for the notificationWatcher +/** + * start the regular notification checks + **/ +function startNotificationWatcher() { + notificationCheck(force=true); + notificationWatcher = setInterval(notificationCheck, 1000); +} + +/** + * stop the regular notification checks + **/ +function stopNotificationWatcher() { + clearInterval(notificationWatcher); +} + + var notificationUpdateTic = 0; /** * The notification checker is initiated when the document is loaded. It checks if there are unread notifications From 44d08d77aa2ff9017865e745e4b4096e1f09824f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 22:35:37 +0100 Subject: [PATCH 043/157] maybe the watcher should be running from the start --- InvenTree/InvenTree/static/script/inventree/inventree.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 0e7c09c32d..d6d639f03e 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -218,6 +218,9 @@ function inventreeDocReady() { // Display any cached alert messages showCachedAlerts(); + // start watcher + startNotificationWatcher(); + // always refresh when the focus returns $(document).focus(function(){ startNotificationWatcher(); From 194c5c58577289fd7c2c188bf442d8b1acb1645e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 22:36:02 +0100 Subject: [PATCH 044/157] remove logging --- InvenTree/InvenTree/static/script/inventree/notification.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index 4fc9b0ac77..d3ff9defe8 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -147,8 +147,6 @@ var notificationUpdateTic = 0; function notificationCheck(force = false) { notificationUpdateTic = notificationUpdateTic + 1; - console.log(notificationUpdateTic); - // refresh if forced or // if in focus and was not refreshed in the last 5 seconds if (force || (document.hasFocus() && notificationUpdateTic >= 5)) { From 679647433a0028349f3662de6addf9d9238cc938 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 22:49:30 +0100 Subject: [PATCH 045/157] smaller read button --- .../InvenTree/static/script/inventree/notification.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index d3ff9defe8..516d438a54 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -209,8 +209,9 @@ function updateNotificationReadState(btn, panel_caller=false) { * arguments: * - pk: primary key of the notification * - state: current state of the notification (read / unread) -> just pass what you were handed by the api + * - small: should the button be small **/ -function getReadEditButton(pk, state) { +function getReadEditButton(pk, state, small=false) { if (state) { var bReadText = '{% trans "Mark as unread" %}'; var bReadIcon = 'fas fa-bookmark icon-red'; @@ -220,7 +221,9 @@ function getReadEditButton(pk, state) { var bReadIcon = 'far fa-bookmark icon-green'; var bReadTarget = 'read'; } - return ``; + + var style = (small) ? 'btn-sm ' : ''; + return ``; } /** @@ -256,7 +259,7 @@ function openNotificationPanel() { } html += '
    '; html += `${item.age_human}`; - html += getReadEditButton(item.pk, item.read); + html += getReadEditButton(item.pk, item.read, true); html += "
    "; }); From cefc4af86193b51807a7d0f796827ce4e8241334 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 23:19:58 +0100 Subject: [PATCH 046/157] add delete button for notifications --- .../InvenTree/notifications/notifications.html | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index e6b15a9ff0..4b34f8f30b 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -90,7 +90,8 @@ function loadNotificationTable(table, options={}) { { formatter: function(value, row, index, field) { var bRead = getReadEditButton(row.pk, row.read) - var html = "
    " + bRead + "
    "; + var bDel = ""; + var html = "
    " + bRead + bDel + "
    "; return html; } } @@ -126,5 +127,15 @@ $("#history-refresh").on('click', function() { $("#history-table").bootstrapTable('refresh'); }); +$("#history-table").on('click', '.notification-delete', function() { + constructForm(`/api/notifications/${$(this).attr('pk')}/`, { + method: 'DELETE', + title: '{% trans "Delete Notification" %}', + onSuccess: function(data) { + updateNotificationTables(); + } + }); +}); + enableSidebar('notifications'); {% endblock %} From a653f322b3c0bf8dfdc27a1415996f47c79e3d9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 23:22:20 +0100 Subject: [PATCH 047/157] show delete button only selective --- .../InvenTree/notifications/notifications.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index 4b34f8f30b..ee1f150ae9 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -29,7 +29,7 @@ function updateNotificationTables() { // this allows the global notification panel to update the tables window.updateNotifications = updateNotificationTables -function loadNotificationTable(table, options={}) { +function loadNotificationTable(table, options={}, enableDelete=false) { var params = options.params || {}; var read = typeof(params.read) === 'undefined' ? true : params.read; @@ -90,7 +90,11 @@ function loadNotificationTable(table, options={}) { { formatter: function(value, row, index, field) { var bRead = getReadEditButton(row.pk, row.read) - var bDel = ""; + if (enableDelete) { + var bDel = ""; + } else { + var bDel = ''; + } var html = "
    " + bRead + bDel + "
    "; return html; } @@ -121,7 +125,7 @@ loadNotificationTable("#history-table", { name: 'history', url: '{% url 'api-notifications-list' %}', no_matches: function() { return '{% trans "No notification history found" %}'; }, -}); +}, true); $("#history-refresh").on('click', function() { $("#history-table").bootstrapTable('refresh'); From 73c477280883b01021d3a5c56b6f2860ce480271 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 23:52:02 +0100 Subject: [PATCH 048/157] fix focusout on offset / not panel --- InvenTree/InvenTree/static/script/inventree/inventree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index d6d639f03e..c69dd8df07 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -227,7 +227,7 @@ function inventreeDocReady() { }); // kill notification watcher if focus is lost -> respect your users cycles - $(document).focusout(function(){ + $(document).blur(function(){ stopNotificationWatcher(); }); From 88d8acfebd4fdd4a16ce50919413ddf6e144add1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Dec 2021 01:42:11 +0100 Subject: [PATCH 049/157] just use the generic view --- InvenTree/common/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 563c8fc79f..03460d566f 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -172,7 +172,7 @@ class NotificationList(generics.ListAPIView): return queryset -class NotificationDetail(generics.RetrieveDestroyAPIView): +class NotificationDetail(generics.RetrieveUpdateDestroyAPIView): """ Detail view for an individual notification object From a73a4255c2f25698b18d8c6eb296bfbef2b0b6e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Dec 2021 20:36:15 +0100 Subject: [PATCH 050/157] helper to get all inheriting classes --- InvenTree/InvenTree/helpers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 6765180e1a..2e34c8f7d8 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -736,3 +736,18 @@ def get_objectreference(obj, type_ref: str = 'content_type', object_ref: str = ' 'model': str(model_cls._meta.verbose_name), **ret } + + +def inheritors(cls): + """ + Return all classes that are subclasses from the supplied cls + """ + subcls = set() + work = [cls] + while work: + parent = work.pop() + for child in parent.__subclasses__(): + if child not in subcls: + subcls.add(child) + work.append(child) + return subcls From 95b5eee59d388558e7d6e43d8406c6cea6a33375 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Dec 2021 22:25:27 +0100 Subject: [PATCH 051/157] add new more generalized notification method --- InvenTree/common/notifications.py | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 InvenTree/common/notifications.py diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py new file mode 100644 index 0000000000..1fc2db3498 --- /dev/null +++ b/InvenTree/common/notifications.py @@ -0,0 +1,143 @@ +from django.utils.translation import ugettext_lazy as _ +from django.template.loader import render_to_string + +from allauth.account.models import EmailAddress + +from InvenTree.helpers import inheritors +import InvenTree.tasks + + +# region notification classes +# region base classes +class NotificationMethod: + method_name = '' + + def __init__(self, obj, entry_name, receivers) -> None: + # check if a sending fnc is defined + if (not 'send' in self) and (not 'send_bulk' in self): + raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') + + # define arguments + self.obj = obj + self.entry_name = entry_name + self.receiers = receivers + + # gather recipiends + self.recipiends = self.get_recipiends() + + def get_recipiends(self): + return False + + def cleanup(self): + return True + + +class SingleNotificationMethod(NotificationMethod): + def send(self, context): + raise NotImplementedError('The `send` method must be overriden!') + + +class BulkNotificationMethod(NotificationMethod): + def send_bulk(self, context): + raise NotImplementedError('The `send` method must be overriden!') +# endregion + +# region implementations +class EmailNotification(BulkNotificationMethod): + method_name = 'mail' + + def get_recipiends(self): + return EmailAddress.objects.filter( + user__in=self.receiers, + ) + + def send_bulk(self, context): + # TODO: In the future, include the part image in the email template + + if not 'template' in context: + raise NotImplementedError('Templates must be provided in the `context`') + if not 'html' in context['template']: + raise NotImplementedError("template['html'] must be provided in the `context`") + if not 'subject' in context['template']: + raise NotImplementedError("template['subject'] must be provided in the `context`") + + html_message = render_to_string(context['template']['html'], context) + recipients = self.recipiends.values_list('email', flat=True) + + InvenTree.tasks.send_email(context['template']['subject'], '', recipients, html_message=html_message) + + return True +# endregion +# endregion + + +def trigger_notifaction( + obj, entry_name=None, obj_ref='pk', + receivers=None, receiver_fnc=None, receiver_args=[], receiver_kwargs={}, + notification_context={} + ): + """ + Send out an notification + """ + + # set defaults + if not entry_name: + entry_name = obj._meta.modelname + + # resolve objekt reference + obj_ref_value = getattr(obj, obj_ref) + # lets try with some dafaults + if not obj_ref_value: + obj_ref_value = getattr(obj, 'pk') + if not obj_ref_value: + obj_ref_value = getattr(obj, 'id') + if not obj_ref_value: + raise KeyError(f"Could not resolve an object reference for '{str(obj)}' with {obj_ref}, pk, id") + + + # Check if we have notified recently... + delta = timedelta(days=1) + + if NotificationEntry.check_recent(entry_name, obj_ref_value, delta): + logger.info(f"Notification '{entry_name}' has recently been sent for '{str(obj)}' - SKIPPING") + return + + logger.info(f"Gathering users for notification '{entry_name}'") + # Collect possible receivers + if not receivers: + receivers = receiver_fnc(*receiver_args, **receiver_kwargs) + + if receivers: + logger.info(f"Sending notification '{entry_name}' for '{str(obj)}'") + + # collect possible methods + delivery_methods = inheritors(NotificationMethod) + + for method in delivery_methods: + logger.info(f"Triggering method '{method.method_name}'") + try: + deliver_notification(method, obj, entry_name, receivers, notification_context) + except Exception as error: + logger.error(error) + + # save delivery flag + NotificationEntry.notify(entry_name, obj_ref_value) + else: + logger.info(f"No possible users for notification '{entry_name}'") + + +def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receivers, notification_context: dict): + method = cls(obj, entry_name, receivers) + + if method.recipiends and method.recipiends > 0: + logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") + + if 'send_bulk' in method: + method.send_bulk(notification_context) + + elif 'send' in method: + for rec in method.recipiends: + method.send(notification_context) + + else: + raise NotImplementedError('No delivery method found') From 64c01bff828ea8d3d58ea687c0ad912e7906a2c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Dec 2021 22:26:43 +0100 Subject: [PATCH 052/157] refactor to use general function --- InvenTree/part/tasks.py | 55 +++++++++++------------------------------ 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index a7c24b385b..acb85f148b 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -2,17 +2,13 @@ from __future__ import unicode_literals import logging -from datetime import timedelta from django.utils.translation import ugettext_lazy as _ -from django.template.loader import render_to_string -from allauth.account.models import EmailAddress - -from common.models import NotificationEntry import InvenTree.helpers import InvenTree.tasks +import common.notifications import part.models @@ -20,44 +16,23 @@ logger = logging.getLogger("inventree") def notify_low_stock(part: part.models.Part): - """ - Notify users who have starred a part when its stock quantity falls below the minimum threshold - """ + context = { + # Pass the "Part" object through to the template context + 'part': part, + 'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()), + 'templates':{ + 'html': 'email/low_stock_notification.html', + 'subject': "[InvenTree] " + _("Low stock notification"), + }, + } - # Check if we have notified recently... - delta = timedelta(days=1) - - if NotificationEntry.check_recent('part.notify_low_stock', part.pk, delta): - logger.info(f"Low stock notification has recently been sent for '{part.full_name}' - SKIPPING") - return - - logger.info(f"Sending low stock notification email for {part.full_name}") - - # Get a list of users who are subcribed to this part - subscribers = part.get_subscribers() - - emails = EmailAddress.objects.filter( - user__in=subscribers, + common.notifications.trigger_notifaction( + part, + 'part.notify_low_stock', + receiver_fnc=part.get_subscribers, + notification_context=context, ) - # TODO: In the future, include the part image in the email template - - if len(emails) > 0: - logger.info(f"Notify users regarding low stock of {part.name}") - context = { - # Pass the "Part" object through to the template context - 'part': part, - 'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()), - } - - subject = "[InvenTree] " + _("Low stock notification") - html_message = render_to_string('email/low_stock_notification.html', context) - recipients = emails.values_list('email', flat=True) - - InvenTree.tasks.send_email(subject, '', recipients, html_message=html_message) - - NotificationEntry.notify('part.notify_low_stock', part.pk) - def notify_low_stock_if_required(part: part.models.Part): """ From 50a046d3dabe5818823cceb6408025a84ed6997e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 00:06:26 +0100 Subject: [PATCH 053/157] PEP fixes --- InvenTree/common/notifications.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 1fc2db3498..8509ea50cb 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -1,12 +1,18 @@ -from django.utils.translation import ugettext_lazy as _ +import logging +from datetime import timedelta + from django.template.loader import render_to_string from allauth.account.models import EmailAddress from InvenTree.helpers import inheritors +from common.models import NotificationEntry import InvenTree.tasks +logger = logging.getLogger('inventree') + + # region notification classes # region base classes class NotificationMethod: @@ -14,7 +20,7 @@ class NotificationMethod: def __init__(self, obj, entry_name, receivers) -> None: # check if a sending fnc is defined - if (not 'send' in self) and (not 'send_bulk' in self): + if ('send' not in self) and ('send_bulk' not in self): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') # define arguments @@ -42,6 +48,7 @@ class BulkNotificationMethod(NotificationMethod): raise NotImplementedError('The `send` method must be overriden!') # endregion + # region implementations class EmailNotification(BulkNotificationMethod): method_name = 'mail' @@ -54,11 +61,11 @@ class EmailNotification(BulkNotificationMethod): def send_bulk(self, context): # TODO: In the future, include the part image in the email template - if not 'template' in context: + if 'template' not in context: raise NotImplementedError('Templates must be provided in the `context`') - if not 'html' in context['template']: + if 'html' not in context['template']: raise NotImplementedError("template['html'] must be provided in the `context`") - if not 'subject' in context['template']: + if 'subject' not in context['template']: raise NotImplementedError("template['subject'] must be provided in the `context`") html_message = render_to_string(context['template']['html'], context) @@ -71,11 +78,7 @@ class EmailNotification(BulkNotificationMethod): # endregion -def trigger_notifaction( - obj, entry_name=None, obj_ref='pk', - receivers=None, receiver_fnc=None, receiver_args=[], receiver_kwargs={}, - notification_context={} - ): +def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, receiver_fnc=None, receiver_args=[], receiver_kwargs={}, notification_context={}): """ Send out an notification """ @@ -94,7 +97,6 @@ def trigger_notifaction( if not obj_ref_value: raise KeyError(f"Could not resolve an object reference for '{str(obj)}' with {obj_ref}, pk, id") - # Check if we have notified recently... delta = timedelta(days=1) From 25a76d37e7d45a8c9e4e290d151273771ed8e6c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 00:11:02 +0100 Subject: [PATCH 054/157] and PEP again --- InvenTree/part/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index acb85f148b..111cd2eb3b 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -20,7 +20,7 @@ def notify_low_stock(part: part.models.Part): # Pass the "Part" object through to the template context 'part': part, 'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()), - 'templates':{ + 'templates': { 'html': 'email/low_stock_notification.html', 'subject': "[InvenTree] " + _("Low stock notification"), }, From 916ef0d6c4fcabb3c3541ec57ab397833545e34f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 01:26:30 +0100 Subject: [PATCH 055/157] add receiver to single send metod --- InvenTree/common/notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 8509ea50cb..a0cff721bf 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -39,7 +39,7 @@ class NotificationMethod: class SingleNotificationMethod(NotificationMethod): - def send(self, context): + def send(self, receiver, context): raise NotImplementedError('The `send` method must be overriden!') @@ -139,7 +139,7 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver elif 'send' in method: for rec in method.recipiends: - method.send(notification_context) + method.send(rec, notification_context) else: raise NotImplementedError('No delivery method found') From e0462c2bab9dccaf681ebda0dce71a0482c1012c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:23:28 +0100 Subject: [PATCH 056/157] do not run through template classes --- InvenTree/common/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index a0cff721bf..aa58b2e5c8 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -115,7 +115,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece # collect possible methods delivery_methods = inheritors(NotificationMethod) - for method in delivery_methods: + for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.method_name}'") try: deliver_notification(method, obj, entry_name, receivers, notification_context) From 7974559eadc0c89e487751877811207547041485 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:24:33 +0100 Subject: [PATCH 057/157] add tests for testing notifications --- InvenTree/part/test_part.py | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 6397b9162f..30cfce1410 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -17,7 +17,7 @@ from .templatetags import inventree_extras import part.settings -from common.models import InvenTreeSetting +from common.models import InvenTreeSetting, NotificationEntry class TemplateTagTest(TestCase): @@ -464,3 +464,44 @@ class PartSubscriptionTests(TestCase): # Check part self.assertTrue(self.part.is_starred_by(self.user)) + + +class PartNotificationTest(TestCase): + """ Tests for the Part model """ + + fixtures = [ + 'location', + 'category', + 'part', + 'stock' + ] + + def setUp(self): + # Create a user for auth + user = get_user_model() + + self.user = user.objects.create_user( + username='testuser', + email='test@testing.com', + password='password', + is_staff=True + ) + + def test_notification(self): + # There should be no notification runs + self.assertEqual(NotificationEntry.objects.all().count(), 0) + + # Test that notifications run through without errors + self.r1 = Part.objects.get(name='R_2K2_0805') + self.r1.minimum_stock = self.r1.get_stock_count() + 1 # make sure minimum is one higher than current count + self.r1.save() + + # There should be no notification as no-one is subscribed + self.assertEqual(NotificationEntry.objects.all().count(), 0) + + # subscribe and run again + self.r1.set_starred(self.user, True) + self.r1.save() + + # There should be 1 notification + self.assertEqual(NotificationEntry.objects.all().count(), 1) From e678e8bd0571e9e95fa30e28098b75c7e61399a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:32:02 +0100 Subject: [PATCH 058/157] fix init checks --- InvenTree/common/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index aa58b2e5c8..ff476e17bc 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -20,7 +20,7 @@ class NotificationMethod: def __init__(self, obj, entry_name, receivers) -> None: # check if a sending fnc is defined - if ('send' not in self) and ('send_bulk' not in self): + if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') # define arguments From ea5848d174c26de32a435b74e4efa28013228fa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:34:45 +0100 Subject: [PATCH 059/157] add mailadress to user --- InvenTree/part/test_part.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 30cfce1410..c5b9b3f60a 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -3,6 +3,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from allauth.account.models import EmailAddress from django.contrib.auth import get_user_model @@ -486,6 +487,8 @@ class PartNotificationTest(TestCase): password='password', is_staff=True ) + # Add Mailadress + EmailAddress.objects.create(user=self.user, email='test@testing.com') def test_notification(self): # There should be no notification runs From 55a853663672244ae68a97bb4e2b8cf4bc00c08d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:35:10 +0100 Subject: [PATCH 060/157] fix case --- InvenTree/part/test_part.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index c5b9b3f60a..7f9b03c210 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -502,7 +502,7 @@ class PartNotificationTest(TestCase): # There should be no notification as no-one is subscribed self.assertEqual(NotificationEntry.objects.all().count(), 0) - # subscribe and run again + # Subscribe and run again self.r1.set_starred(self.user, True) self.r1.save() From 275569dab447b06ee52f0cf16f0c320423c338c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:36:00 +0100 Subject: [PATCH 061/157] fix mail comparison --- InvenTree/common/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index ff476e17bc..edea0666e0 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -131,7 +131,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receivers, notification_context: dict): method = cls(obj, entry_name, receivers) - if method.recipiends and method.recipiends > 0: + if method.recipiends and method.recipiends.count() > 0: logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") if 'send_bulk' in method: From f0ebc3fdbfcb4d7155a8b1097b6ae204b6f391d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:37:16 +0100 Subject: [PATCH 062/157] fix checks if function exsists --- InvenTree/common/notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index edea0666e0..ec38570fb7 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -134,10 +134,10 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver if method.recipiends and method.recipiends.count() > 0: logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") - if 'send_bulk' in method: + if hasattr(method, 'send_bulk'): method.send_bulk(notification_context) - elif 'send' in method: + elif hasattr(method, 'send'): for rec in method.recipiends: method.send(rec, notification_context) From 2038c2250dc57aa9961f238cab17abf92e144ddb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:42:57 +0100 Subject: [PATCH 063/157] fix notify_low_stock context --- InvenTree/part/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index 111cd2eb3b..2f6b58afa9 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -20,7 +20,7 @@ def notify_low_stock(part: part.models.Part): # Pass the "Part" object through to the template context 'part': part, 'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()), - 'templates': { + 'template': { 'html': 'email/low_stock_notification.html', 'subject': "[InvenTree] " + _("Low stock notification"), }, From 98cf3cd3ad5079df215785aad8052a9b0e7025d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:52:18 +0100 Subject: [PATCH 064/157] log results of delivery --- InvenTree/common/notifications.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index ec38570fb7..6d0e734eee 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -132,14 +132,27 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver method = cls(obj, entry_name, receivers) if method.recipiends and method.recipiends.count() > 0: + # Log start logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") + success = True + success_count = 0 + if hasattr(method, 'send_bulk'): - method.send_bulk(notification_context) + success = method.send_bulk(notification_context) + success_count = method.recipiends.count() elif hasattr(method, 'send'): for rec in method.recipiends: - method.send(rec, notification_context) + if method.send(rec, notification_context): + success_count += 1 + else: + success = False else: raise NotImplementedError('No delivery method found') + + # Log results + logger.info(f"Notified {success_count} users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}' successfully") + if not success: + logger.info("There were some problems") From 6dda000292731183d36cc8d9590e5512f4e00b16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:52:50 +0100 Subject: [PATCH 065/157] fix case --- InvenTree/common/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 6d0e734eee..bc648891cd 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -122,7 +122,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece except Exception as error: logger.error(error) - # save delivery flag + # Set delivery flag NotificationEntry.notify(entry_name, obj_ref_value) else: logger.info(f"No possible users for notification '{entry_name}'") From 8776492aa9e0b22d1cc23b63a401cebec174afea Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 02:54:48 +0100 Subject: [PATCH 066/157] fix docs --- InvenTree/common/notifications.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index bc648891cd..3d0be2be66 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -83,13 +83,13 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece Send out an notification """ - # set defaults + # Set defaults if not entry_name: entry_name = obj._meta.modelname - # resolve objekt reference + # Resolve objekt reference obj_ref_value = getattr(obj, obj_ref) - # lets try with some dafaults + # Try with some defaults if not obj_ref_value: obj_ref_value = getattr(obj, 'pk') if not obj_ref_value: @@ -112,7 +112,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece if receivers: logger.info(f"Sending notification '{entry_name}' for '{str(obj)}'") - # collect possible methods + # Collect possible methods delivery_methods = inheritors(NotificationMethod) for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: @@ -129,6 +129,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receivers, notification_context: dict): + # Init delivery method method = cls(obj, entry_name, receivers) if method.recipiends and method.recipiends.count() > 0: @@ -138,6 +139,7 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver success = True success_count = 0 + # Select delivery method and execute it if hasattr(method, 'send_bulk'): success = method.send_bulk(notification_context) success_count = method.recipiends.count() From 61b6590f52013bda00da69e5729977bb4350a42c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 09:42:50 +0100 Subject: [PATCH 067/157] raise if not implemented --- InvenTree/common/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 3d0be2be66..865e2ff65d 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -32,7 +32,7 @@ class NotificationMethod: self.recipiends = self.get_recipiends() def get_recipiends(self): - return False + raise NotImplementedError('The `get_recipiends` method must be implemented!') def cleanup(self): return True From 7ed18c1bfde9e0f7d5a799b90392244a8b02ae81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 09:44:35 +0100 Subject: [PATCH 068/157] spell fixing --- InvenTree/common/notifications.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 865e2ff65d..500ad10dd4 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -28,11 +28,11 @@ class NotificationMethod: self.entry_name = entry_name self.receiers = receivers - # gather recipiends - self.recipiends = self.get_recipiends() + # gather recipients + self.recipients = self.get_recipients() - def get_recipiends(self): - raise NotImplementedError('The `get_recipiends` method must be implemented!') + def get_recipients(self): + raise NotImplementedError('The `get_recipients` method must be implemented!') def cleanup(self): return True @@ -53,7 +53,7 @@ class BulkNotificationMethod(NotificationMethod): class EmailNotification(BulkNotificationMethod): method_name = 'mail' - def get_recipiends(self): + def get_recipients(self): return EmailAddress.objects.filter( user__in=self.receiers, ) @@ -69,7 +69,7 @@ class EmailNotification(BulkNotificationMethod): raise NotImplementedError("template['subject'] must be provided in the `context`") html_message = render_to_string(context['template']['html'], context) - recipients = self.recipiends.values_list('email', flat=True) + recipients = self.recipients.values_list('email', flat=True) InvenTree.tasks.send_email(context['template']['subject'], '', recipients, html_message=html_message) @@ -132,7 +132,7 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver # Init delivery method method = cls(obj, entry_name, receivers) - if method.recipiends and method.recipiends.count() > 0: + if method.recipients and method.recipients.count() > 0: # Log start logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") @@ -142,10 +142,10 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver # Select delivery method and execute it if hasattr(method, 'send_bulk'): success = method.send_bulk(notification_context) - success_count = method.recipiends.count() + success_count = method.recipients.count() elif hasattr(method, 'send'): - for rec in method.recipiends: + for rec in method.recipients: if method.send(rec, notification_context): success_count += 1 else: From 4e29fed164dfa9c78f504ab30ad4d43facbbec6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 09:46:12 +0100 Subject: [PATCH 069/157] spell fixing --- InvenTree/common/notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 500ad10dd4..0469066fda 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -26,7 +26,7 @@ class NotificationMethod: # define arguments self.obj = obj self.entry_name = entry_name - self.receiers = receivers + self.receivers = receivers # gather recipients self.recipients = self.get_recipients() @@ -55,7 +55,7 @@ class EmailNotification(BulkNotificationMethod): def get_recipients(self): return EmailAddress.objects.filter( - user__in=self.receiers, + user__in=self.receivers, ) def send_bulk(self, context): From 803936130c8caa1c386c3a4a87f4655186089bc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 09:46:50 +0100 Subject: [PATCH 070/157] doc spell fixing --- InvenTree/common/notifications.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 0469066fda..3478a71b32 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -19,16 +19,16 @@ class NotificationMethod: method_name = '' def __init__(self, obj, entry_name, receivers) -> None: - # check if a sending fnc is defined + # Check if a sending fnc is defined if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') - # define arguments + # Define arguments self.obj = obj self.entry_name = entry_name self.receivers = receivers - # gather recipients + # Gather recipients self.recipients = self.get_recipients() def get_recipients(self): From 1b9ca41b4690cbb635fe50e621b664ff14ec31b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 09:52:30 +0100 Subject: [PATCH 071/157] added setup function to notification method and added calls to deliver_method --- InvenTree/common/notifications.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 3478a71b32..1c0545c63e 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -34,6 +34,9 @@ class NotificationMethod: def get_recipients(self): raise NotImplementedError('The `get_recipients` method must be implemented!') + def setup(self): + return True + def cleanup(self): return True @@ -136,6 +139,9 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver # Log start logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") + # Run setup for delivery method + method.setup() + success = True success_count = 0 @@ -154,6 +160,9 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver else: raise NotImplementedError('No delivery method found') + # Run cleanup for delivery method + method.cleanup() + # Log results logger.info(f"Notified {success_count} users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}' successfully") if not success: From eb5a87c530d45b858b5be577ce838ce1284ce68d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 10:05:12 +0100 Subject: [PATCH 072/157] rename test --- InvenTree/common/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index c20dc5d126..48838415b5 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -90,7 +90,7 @@ class SettingsTest(TestCase): raise ValueError(f'Non-boolean default value specified for {key}') -class NotificationTest(TestCase): +class NotificationEntryTest(TestCase): def test_check_notification_entries(self): From 004687a29fb4a169ee0fc5e77213d0d7159b3a94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 10:14:04 +0100 Subject: [PATCH 073/157] test implementation checks --- InvenTree/common/tests.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 48838415b5..119edaabd0 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model from .models import InvenTreeSetting from .models import NotificationEntry - +from .notifications import NotificationMethod class SettingsTest(TestCase): """ @@ -108,3 +108,23 @@ class NotificationEntryTest(TestCase): self.assertFalse(NotificationEntry.check_recent('test.notification2', 1, delta)) self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta)) + + +class NotificationTests(TestCase): + + def test_NotificationMethod(self): + """ensure the implementation requirements are tested""" + + class FalseNotificationMethod(NotificationMethod): + pass + + class AnotherFalseNotificationMethod(NotificationMethod): + def send(self): + pass + + with self.assertRaises(NotImplementedError): + FalseNotificationMethod('', '', '') + + with self.assertRaises(NotImplementedError): + AnotherFalseNotificationMethod('', '', '') + From 84499f4b10eb367db136b56045925cc7a45b6738 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 10:22:39 +0100 Subject: [PATCH 074/157] better description --- InvenTree/common/tests.py | 1 + InvenTree/part/test_part.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 119edaabd0..5b0249272e 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -128,3 +128,4 @@ class NotificationTests(TestCase): with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '') +# A integration test for notifications is provided in test_part.PartNotificationTest diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 7f9b03c210..c17084c0c7 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -468,7 +468,7 @@ class PartSubscriptionTests(TestCase): class PartNotificationTest(TestCase): - """ Tests for the Part model """ + """ Integration test for part notifications """ fixtures = [ 'location', From ae13672273fd7a4676e58410b9855dbe7cc3ee71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 10:52:21 +0100 Subject: [PATCH 075/157] refactor to enable use in notification tests --- InvenTree/part/test_part.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index c17084c0c7..7f9b11478a 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -467,8 +467,8 @@ class PartSubscriptionTests(TestCase): self.assertTrue(self.part.is_starred_by(self.user)) -class PartNotificationTest(TestCase): - """ Integration test for part notifications """ +class BaseNotificationIntegrationTest(TestCase): + """ Integration test for notifications """ fixtures = [ 'location', @@ -490,21 +490,29 @@ class PartNotificationTest(TestCase): # Add Mailadress EmailAddress.objects.create(user=self.user, email='test@testing.com') - def test_notification(self): + # Define part that will be tested + self.part = Part.objects.get(name='R_2K2_0805') + + def _notification_run(self): # There should be no notification runs self.assertEqual(NotificationEntry.objects.all().count(), 0) # Test that notifications run through without errors - self.r1 = Part.objects.get(name='R_2K2_0805') - self.r1.minimum_stock = self.r1.get_stock_count() + 1 # make sure minimum is one higher than current count - self.r1.save() + self.part.minimum_stock = self.part.get_stock_count() + 1 # make sure minimum is one higher than current count + self.part.save() # There should be no notification as no-one is subscribed self.assertEqual(NotificationEntry.objects.all().count(), 0) # Subscribe and run again - self.r1.set_starred(self.user, True) - self.r1.save() + self.part.set_starred(self.user, True) + self.part.save() # There should be 1 notification self.assertEqual(NotificationEntry.objects.all().count(), 1) + +class PartNotificationTest(BaseNotificationIntegrationTest): + """ Integration test for part notifications """ + + def test_notification(self): + self._notification_run() From 162d4347e7222536d750d55775f4a923b6dc9f74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 10:53:46 +0100 Subject: [PATCH 076/157] more implementation tests --- InvenTree/common/tests.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 5b0249272e..9138af7cf0 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -8,7 +8,10 @@ from django.contrib.auth import get_user_model from .models import InvenTreeSetting from .models import NotificationEntry -from .notifications import NotificationMethod +from .notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod + +from part.test_part import BaseNotificationIntegrationTest + class SettingsTest(TestCase): """ @@ -110,7 +113,7 @@ class NotificationEntryTest(TestCase): self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta)) -class NotificationTests(TestCase): +class NotificationTests(BaseNotificationIntegrationTest): def test_NotificationMethod(self): """ensure the implementation requirements are tested""" @@ -128,4 +131,23 @@ class NotificationTests(TestCase): with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '') + def test_SingleNotificationMethod(self): + """ensure the implementation requirements are tested""" + + class WrongImplementation(SingleNotificationMethod): + pass + + with self.assertRaises(NotImplementedError): + self._notification_run() + + def test_BulkNotificationMethod(self): + """ensure the implementation requirements are tested""" + + class WrongImplementation(BulkNotificationMethod): + pass + + with self.assertRaises(NotImplementedError): + self._notification_run() + + # A integration test for notifications is provided in test_part.PartNotificationTest From 65d5ed1bdf7e24b1299445bd8bff264b9ce41e68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 10:54:04 +0100 Subject: [PATCH 077/157] raise not implemented --- InvenTree/common/notifications.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 1c0545c63e..49071ab0c3 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -122,6 +122,8 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece logger.info(f"Triggering method '{method.method_name}'") try: deliver_notification(method, obj, entry_name, receivers, notification_context) + except NotImplementedError as error: + raise error except Exception as error: logger.error(error) From f4f390f4c26d17a92b21b8947d1eb6e382cfda8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Dec 2021 01:58:27 +0100 Subject: [PATCH 078/157] PEP fix --- InvenTree/part/test_part.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 7f9b11478a..009798b6ff 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -511,6 +511,7 @@ class BaseNotificationIntegrationTest(TestCase): # There should be 1 notification self.assertEqual(NotificationEntry.objects.all().count(), 1) + class PartNotificationTest(BaseNotificationIntegrationTest): """ Integration test for part notifications """ From 79bab93afc8807f3f2532eced7c4e43e12b66bef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Dec 2021 02:01:17 +0100 Subject: [PATCH 079/157] refactor into new test file --- InvenTree/common/test_notifications.py | 45 ++++++++++++++++++++++++++ InvenTree/common/tests.py | 43 ------------------------ 2 files changed, 45 insertions(+), 43 deletions(-) create mode 100644 InvenTree/common/test_notifications.py diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py new file mode 100644 index 0000000000..99a12a52a0 --- /dev/null +++ b/InvenTree/common/test_notifications.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod +from part.test_part import BaseNotificationIntegrationTest + + +class NotificationTests(BaseNotificationIntegrationTest): + + def test_NotificationMethod(self): + """ensure the implementation requirements are tested""" + + class FalseNotificationMethod(NotificationMethod): + pass + + class AnotherFalseNotificationMethod(NotificationMethod): + def send(self): + pass + + with self.assertRaises(NotImplementedError): + FalseNotificationMethod('', '', '') + + with self.assertRaises(NotImplementedError): + AnotherFalseNotificationMethod('', '', '') + + def test_SingleNotificationMethod(self): + """ensure the implementation requirements are tested""" + + class WrongImplementation(SingleNotificationMethod): + pass + + with self.assertRaises(NotImplementedError): + self._notification_run() + + def test_BulkNotificationMethod(self): + """ensure the implementation requirements are tested""" + + class WrongImplementation(BulkNotificationMethod): + pass + + with self.assertRaises(NotImplementedError): + self._notification_run() + + +# A integration test for notifications is provided in test_part.PartNotificationTest diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 9138af7cf0..48838415b5 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -8,9 +8,6 @@ from django.contrib.auth import get_user_model from .models import InvenTreeSetting from .models import NotificationEntry -from .notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod - -from part.test_part import BaseNotificationIntegrationTest class SettingsTest(TestCase): @@ -111,43 +108,3 @@ class NotificationEntryTest(TestCase): self.assertFalse(NotificationEntry.check_recent('test.notification2', 1, delta)) self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta)) - - -class NotificationTests(BaseNotificationIntegrationTest): - - def test_NotificationMethod(self): - """ensure the implementation requirements are tested""" - - class FalseNotificationMethod(NotificationMethod): - pass - - class AnotherFalseNotificationMethod(NotificationMethod): - def send(self): - pass - - with self.assertRaises(NotImplementedError): - FalseNotificationMethod('', '', '') - - with self.assertRaises(NotImplementedError): - AnotherFalseNotificationMethod('', '', '') - - def test_SingleNotificationMethod(self): - """ensure the implementation requirements are tested""" - - class WrongImplementation(SingleNotificationMethod): - pass - - with self.assertRaises(NotImplementedError): - self._notification_run() - - def test_BulkNotificationMethod(self): - """ensure the implementation requirements are tested""" - - class WrongImplementation(BulkNotificationMethod): - pass - - with self.assertRaises(NotImplementedError): - self._notification_run() - - -# A integration test for notifications is provided in test_part.PartNotificationTest From 2917af971592cd2983b83fadd95307d0d649f716 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Dec 2021 02:02:51 +0100 Subject: [PATCH 080/157] add marker statment to wrong implementations --- InvenTree/common/test_notifications.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 99a12a52a0..4785de8908 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -27,7 +27,9 @@ class NotificationTests(BaseNotificationIntegrationTest): """ensure the implementation requirements are tested""" class WrongImplementation(SingleNotificationMethod): - pass + def setup(self): + print('running setup on WrongImplementation') + return super().setup() with self.assertRaises(NotImplementedError): self._notification_run() @@ -36,7 +38,9 @@ class NotificationTests(BaseNotificationIntegrationTest): """ensure the implementation requirements are tested""" class WrongImplementation(BulkNotificationMethod): - pass + def setup(self): + print('running setup on WrongImplementation') + return super().setup() with self.assertRaises(NotImplementedError): self._notification_run() From 6e464447f5032292be2dd0bbbb114157e7fbc861 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Dec 2021 07:51:21 +0100 Subject: [PATCH 081/157] markers for notification tests --- InvenTree/common/notifications.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 49071ab0c3..ec24df0bf2 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -1,5 +1,6 @@ import logging from datetime import timedelta +from django.conf import settings from django.template.loader import render_to_string @@ -117,6 +118,9 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece # Collect possible methods delivery_methods = inheritors(NotificationMethod) + if settings.TESTING: + print('TESTING') + print(delivery_methods) for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.method_name}'") From 33b7d9b845b14c5fc38f8b0fd20d0dbb62fd3a6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 00:43:46 +0100 Subject: [PATCH 082/157] more debug prints --- InvenTree/common/notifications.py | 5 ++++- InvenTree/common/test_notifications.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index ec24df0bf2..0d61e3d0c7 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -118,8 +118,9 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece # Collect possible methods delivery_methods = inheritors(NotificationMethod) + + # TODO remove -> this is for debugging the delivery method runfs if settings.TESTING: - print('TESTING') print(delivery_methods) for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: @@ -127,8 +128,10 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece try: deliver_notification(method, obj, entry_name, receivers, notification_context) except NotImplementedError as error: + print('NotImplementedError') raise error except Exception as error: + print(error) logger.error(error) # Set delivery flag diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 4785de8908..8eff1ef7fc 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -25,6 +25,7 @@ class NotificationTests(BaseNotificationIntegrationTest): def test_SingleNotificationMethod(self): """ensure the implementation requirements are tested""" + print('TESTING SingleNotificationMethod') class WrongImplementation(SingleNotificationMethod): def setup(self): @@ -36,6 +37,7 @@ class NotificationTests(BaseNotificationIntegrationTest): def test_BulkNotificationMethod(self): """ensure the implementation requirements are tested""" + print('TESTING BulkNotificationMethod') class WrongImplementation(BulkNotificationMethod): def setup(self): From 0f3a78c7be0df763472fddec1e1fd7d103e63faa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 00:58:13 +0100 Subject: [PATCH 083/157] refactor method name --- InvenTree/common/notifications.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 0d61e3d0c7..63999c0c5a 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -17,7 +17,7 @@ logger = logging.getLogger('inventree') # region notification classes # region base classes class NotificationMethod: - method_name = '' + METHOD_NAME = '' def __init__(self, obj, entry_name, receivers) -> None: # Check if a sending fnc is defined @@ -55,7 +55,7 @@ class BulkNotificationMethod(NotificationMethod): # region implementations class EmailNotification(BulkNotificationMethod): - method_name = 'mail' + METHOD_NAME = 'mail' def get_recipients(self): return EmailAddress.objects.filter( @@ -124,7 +124,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece print(delivery_methods) for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: - logger.info(f"Triggering method '{method.method_name}'") + logger.info(f"Triggering method '{method.METHOD_NAME}'") try: deliver_notification(method, obj, entry_name, receivers, notification_context) except NotImplementedError as error: @@ -146,7 +146,7 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver if method.recipients and method.recipients.count() > 0: # Log start - logger.info(f"Notify users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}'") + logger.info(f"Notify users via '{method.METHOD_NAME}' for notification '{entry_name}' for '{str(obj)}'") # Run setup for delivery method method.setup() @@ -173,6 +173,6 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver method.cleanup() # Log results - logger.info(f"Notified {success_count} users via '{method.method_name}' for notification '{entry_name}' for '{str(obj)}' successfully") + logger.info(f"Notified {success_count} users via '{method.METHOD_NAME}' for notification '{entry_name}' for '{str(obj)}' successfully") if not success: logger.info("There were some problems") From e18f7d49ccca1b153fc36fb5f22cb35d88d3e2e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 01:07:07 +0100 Subject: [PATCH 084/157] make mehtod name mandatory --- InvenTree/common/notifications.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 63999c0c5a..bae6f6a763 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta -from django.conf import settings +from django.conf import settings from django.template.loader import render_to_string from allauth.account.models import EmailAddress @@ -24,6 +24,10 @@ class NotificationMethod: if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') + # No method name is no good + if self.METHOD_NAME in ('', None): + raise NotImplementedError(f'The NotificationMethod {self.__class__} did not provide a METHOD_NAME') + # Define arguments self.obj = obj self.entry_name = entry_name From 4e07b4bf721c457f5d414f1c930c22c40cc99b28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 01:10:46 +0100 Subject: [PATCH 085/157] fix tests + method name test --- InvenTree/common/test_notifications.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 8eff1ef7fc..0e645585ad 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -11,23 +11,33 @@ class NotificationTests(BaseNotificationIntegrationTest): """ensure the implementation requirements are tested""" class FalseNotificationMethod(NotificationMethod): - pass + METHOD_NAME = 'FalseNotification' class AnotherFalseNotificationMethod(NotificationMethod): + METHOD_NAME = 'AnotherFalseNotification' + def send(self): pass + class NoNameNotificationMethod(NotificationMethod): + pass + with self.assertRaises(NotImplementedError): FalseNotificationMethod('', '', '') with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '') + with self.assertRaises(NotImplementedError): + NoNameNotificationMethod('', '', '') + def test_SingleNotificationMethod(self): """ensure the implementation requirements are tested""" print('TESTING SingleNotificationMethod') class WrongImplementation(SingleNotificationMethod): + METHOD_NAME = 'WrongImplementation1' + def setup(self): print('running setup on WrongImplementation') return super().setup() @@ -40,6 +50,8 @@ class NotificationTests(BaseNotificationIntegrationTest): print('TESTING BulkNotificationMethod') class WrongImplementation(BulkNotificationMethod): + METHOD_NAME = 'WrongImplementation2' + def setup(self): print('running setup on WrongImplementation') return super().setup() From 1f7a75ca9242cca743dc86688ebe9234c96578c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 01:10:56 +0100 Subject: [PATCH 086/157] docs are never bad --- InvenTree/common/test_notifications.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 0e645585ad..aceb25dafb 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -22,12 +22,15 @@ class NotificationTests(BaseNotificationIntegrationTest): class NoNameNotificationMethod(NotificationMethod): pass + # no send / send bulk with self.assertRaises(NotImplementedError): FalseNotificationMethod('', '', '') + # no gathering with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '') + # no METHOD_NAME with self.assertRaises(NotImplementedError): NoNameNotificationMethod('', '', '') From c803fdaab158da0a1a29cbebeb3c954738cbd2e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 01:39:58 +0100 Subject: [PATCH 087/157] PEP fix --- InvenTree/common/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index aceb25dafb..328d0e6ae1 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -11,7 +11,7 @@ class NotificationTests(BaseNotificationIntegrationTest): """ensure the implementation requirements are tested""" class FalseNotificationMethod(NotificationMethod): - METHOD_NAME = 'FalseNotification' + METHOD_NAME = 'FalseNotification' class AnotherFalseNotificationMethod(NotificationMethod): METHOD_NAME = 'AnotherFalseNotification' From dceea757e1adf2ce337ada48c91ea1737e049204 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 02:12:51 +0100 Subject: [PATCH 088/157] create a notification message! --- InvenTree/common/notifications.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index bae6f6a763..86b9a922b1 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -7,7 +7,7 @@ from django.template.loader import render_to_string from allauth.account.models import EmailAddress from InvenTree.helpers import inheritors -from common.models import NotificationEntry +from common.models import NotificationEntry, NotificationMessage import InvenTree.tasks @@ -82,6 +82,22 @@ class EmailNotification(BulkNotificationMethod): InvenTree.tasks.send_email(context['template']['subject'], '', recipients, html_message=html_message) return True + +class UIMessageNotification(SingleNotificationMethod): + METHOD_NAME = 'ui_message' + + def get_recipients(self): + return self.receivers + + def send(self, receiver, context): + NotificationMessage.objects.create( + target_object = self.obj, + source_object = receiver, + user = receiver, + category = self.entry_name, + name = context['name'], + message = context['message'], + ) # endregion # endregion From febc51466aec25a95ca35506fef6132d7a197e2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 02:13:53 +0100 Subject: [PATCH 089/157] use a more gnerall counting method --- InvenTree/common/notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 86b9a922b1..096cf86814 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -164,7 +164,7 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver # Init delivery method method = cls(obj, entry_name, receivers) - if method.recipients and method.recipients.count() > 0: + if method.recipients and len(method.recipients) > 0: # Log start logger.info(f"Notify users via '{method.METHOD_NAME}' for notification '{entry_name}' for '{str(obj)}'") @@ -177,7 +177,7 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver # Select delivery method and execute it if hasattr(method, 'send_bulk'): success = method.send_bulk(notification_context) - success_count = method.recipients.count() + success_count = len(method.recipients) elif hasattr(method, 'send'): for rec in method.recipients: From 1ba7110cda89744f98c86a6935f429b33fefe905 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 02:19:12 +0100 Subject: [PATCH 090/157] change templates to new way --- InvenTree/part/tasks.py | 7 +++++-- InvenTree/templates/email/low_stock_notification.html | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index 2f6b58afa9..bb950fc098 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -16,13 +16,16 @@ logger = logging.getLogger("inventree") def notify_low_stock(part: part.models.Part): + name = _("Low stock notification") + message = _(f'The available stock for {part.name} has fallen below the configured minimum level') context = { - # Pass the "Part" object through to the template context 'part': part, + 'name': name, + 'message': message, 'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()), 'template': { 'html': 'email/low_stock_notification.html', - 'subject': "[InvenTree] " + _("Low stock notification"), + 'subject': "[InvenTree] " + name, }, } diff --git a/InvenTree/templates/email/low_stock_notification.html b/InvenTree/templates/email/low_stock_notification.html index f922187f33..01c475cd87 100644 --- a/InvenTree/templates/email/low_stock_notification.html +++ b/InvenTree/templates/email/low_stock_notification.html @@ -4,7 +4,7 @@ {% load inventree_extras %} {% block title %} -{% blocktrans with part=part.name %} The available stock for {{ part }} has fallen below the configured minimum level{% endblocktrans %} +{{ message }} {% if link %}

    {% trans "Click on the following link to view this part" %}: {{ link }}

    {% endif %} From 1ef7163b948246b9d7a6275d3940fa4910206fed Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 02:39:41 +0100 Subject: [PATCH 091/157] remove one uncovered line :-) --- InvenTree/common/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 328d0e6ae1..1200c3be3c 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -17,7 +17,7 @@ class NotificationTests(BaseNotificationIntegrationTest): METHOD_NAME = 'AnotherFalseNotification' def send(self): - pass + """a comment so we do not need a pass""" class NoNameNotificationMethod(NotificationMethod): pass From 3239e60702e92bb88e08697f0e0ba14255fafd08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:37:13 +0100 Subject: [PATCH 092/157] add comments to show execution flow --- InvenTree/common/notifications.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 096cf86814..7f99ca353d 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -42,6 +42,9 @@ class NotificationMethod: def setup(self): return True + # def send(self, receiver) + # def send_bulk(self) + def cleanup(self): return True From 1cfc03d69849deeb54359137cc325dd6b325f32a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:40:53 +0100 Subject: [PATCH 093/157] add a general context checker --- InvenTree/common/notifications.py | 76 ++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 7f99ca353d..546c30431a 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -18,8 +18,10 @@ logger = logging.getLogger('inventree') # region base classes class NotificationMethod: METHOD_NAME = '' + CONTEXT_BUILTIN = ['name', 'message', ] + CONTEXT_EXTRA = [] - def __init__(self, obj, entry_name, receivers) -> None: + def __init__(self, obj, entry_name, receivers, context) -> None: # Check if a sending fnc is defined if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') @@ -32,10 +34,46 @@ class NotificationMethod: self.obj = obj self.entry_name = entry_name self.receivers = receivers + self.context = self.check_context(context) # Gather recipients self.recipients = self.get_recipients() + def check_context(self, context): + def check(ref, obj): + # the obj is not accesible so we are on the end + if not isinstance(obj, (list, dict, tuple, )): + return ref + + # check if the ref exsists + if isinstance(ref, str): + if not obj.get(ref): + return ref + return False + + # nested + elif isinstance(ref, (tuple, list)): + if len(ref) == 1: + return check(ref[0], obj) + ret = check(ref[0], obj) + if ret: + return ret + return check(ref[1:], obj[ref[0]]) + + # other cases -> raise + raise NotImplementedError('This type can not be used as a context reference') + + missing = [] + for item in (*self.CONTEXT_BUILTIN, *self.CONTEXT_EXTRA): + ret = check(item, context) + if ret: + missing.append(ret) + + if missing: + raise NotImplementedError(f'The `context` is missing the following items:\n{missing}') + + return context + def get_recipients(self): raise NotImplementedError('The `get_recipients` method must be implemented!') @@ -50,12 +88,12 @@ class NotificationMethod: class SingleNotificationMethod(NotificationMethod): - def send(self, receiver, context): + def send(self, receiver): raise NotImplementedError('The `send` method must be overriden!') class BulkNotificationMethod(NotificationMethod): - def send_bulk(self, context): + def send_bulk(self): raise NotImplementedError('The `send` method must be overriden!') # endregion @@ -63,26 +101,22 @@ class BulkNotificationMethod(NotificationMethod): # region implementations class EmailNotification(BulkNotificationMethod): METHOD_NAME = 'mail' + CONTEXT_EXTRA = [ + ('template', ), + ('template', 'html', ), + ('template', 'subject', ), + ] def get_recipients(self): return EmailAddress.objects.filter( user__in=self.receivers, ) - def send_bulk(self, context): - # TODO: In the future, include the part image in the email template - - if 'template' not in context: - raise NotImplementedError('Templates must be provided in the `context`') - if 'html' not in context['template']: - raise NotImplementedError("template['html'] must be provided in the `context`") - if 'subject' not in context['template']: - raise NotImplementedError("template['subject'] must be provided in the `context`") - - html_message = render_to_string(context['template']['html'], context) + def send_bulk(self): + html_message = render_to_string(self.context['template']['html'], self.context) recipients = self.recipients.values_list('email', flat=True) - InvenTree.tasks.send_email(context['template']['subject'], '', recipients, html_message=html_message) + InvenTree.tasks.send_email(self.context['template']['subject'], '', recipients, html_message=html_message) return True @@ -92,14 +126,14 @@ class UIMessageNotification(SingleNotificationMethod): def get_recipients(self): return self.receivers - def send(self, receiver, context): + def send(self, receiver): NotificationMessage.objects.create( target_object = self.obj, source_object = receiver, user = receiver, category = self.entry_name, - name = context['name'], - message = context['message'], + name = self.context['name'], + message = self.context['message'], ) # endregion # endregion @@ -165,7 +199,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receivers, notification_context: dict): # Init delivery method - method = cls(obj, entry_name, receivers) + method = cls(obj, entry_name, receivers, notification_context) if method.recipients and len(method.recipients) > 0: # Log start @@ -179,12 +213,12 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receiver # Select delivery method and execute it if hasattr(method, 'send_bulk'): - success = method.send_bulk(notification_context) + success = method.send_bulk() success_count = len(method.recipients) elif hasattr(method, 'send'): for rec in method.recipients: - if method.send(rec, notification_context): + if method.send(rec): success_count += 1 else: success = False From 057c880377fe731fe68f78d2b376f8e219077815 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:41:05 +0100 Subject: [PATCH 094/157] fix test calls --- InvenTree/common/test_notifications.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 1200c3be3c..d899dd14a0 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -24,15 +24,15 @@ class NotificationTests(BaseNotificationIntegrationTest): # no send / send bulk with self.assertRaises(NotImplementedError): - FalseNotificationMethod('', '', '') + FalseNotificationMethod('', '', '', '', ) # no gathering with self.assertRaises(NotImplementedError): - AnotherFalseNotificationMethod('', '', '') + AnotherFalseNotificationMethod('', '', '', '', ) # no METHOD_NAME with self.assertRaises(NotImplementedError): - NoNameNotificationMethod('', '', '') + NoNameNotificationMethod('', '', '', '', ) def test_SingleNotificationMethod(self): """ensure the implementation requirements are tested""" From fc1fa775c3cc1c535d5ad4c86b08179d65bfeefe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:43:14 +0100 Subject: [PATCH 095/157] rename --- InvenTree/common/notifications.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 546c30431a..5012e908fe 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -21,7 +21,7 @@ class NotificationMethod: CONTEXT_BUILTIN = ['name', 'message', ] CONTEXT_EXTRA = [] - def __init__(self, obj, entry_name, receivers, context) -> None: + def __init__(self, obj, entry_name, targets, context) -> None: # Check if a sending fnc is defined if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') @@ -33,7 +33,7 @@ class NotificationMethod: # Define arguments self.obj = obj self.entry_name = entry_name - self.receivers = receivers + self.targets = targets self.context = self.check_context(context) # Gather recipients @@ -109,7 +109,7 @@ class EmailNotification(BulkNotificationMethod): def get_recipients(self): return EmailAddress.objects.filter( - user__in=self.receivers, + user__in=self.targets, ) def send_bulk(self): @@ -124,7 +124,7 @@ class UIMessageNotification(SingleNotificationMethod): METHOD_NAME = 'ui_message' def get_recipients(self): - return self.receivers + return self.targets def send(self, receiver): NotificationMessage.objects.create( @@ -139,7 +139,7 @@ class UIMessageNotification(SingleNotificationMethod): # endregion -def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, receiver_fnc=None, receiver_args=[], receiver_kwargs={}, notification_context={}): +def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, receiver_fnc=None, receiver_args=[], receiver_kwargs={}, notification_context={}): """ Send out an notification """ @@ -166,11 +166,11 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece return logger.info(f"Gathering users for notification '{entry_name}'") - # Collect possible receivers - if not receivers: - receivers = receiver_fnc(*receiver_args, **receiver_kwargs) + # Collect possible targets + if not targets: + targets = receiver_fnc(*receiver_args, **receiver_kwargs) - if receivers: + if targets: logger.info(f"Sending notification '{entry_name}' for '{str(obj)}'") # Collect possible methods @@ -183,7 +183,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.METHOD_NAME}'") try: - deliver_notification(method, obj, entry_name, receivers, notification_context) + deliver_notification(method, obj, entry_name, targets, notification_context) except NotImplementedError as error: print('NotImplementedError') raise error @@ -197,9 +197,9 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', receivers=None, rece logger.info(f"No possible users for notification '{entry_name}'") -def deliver_notification(cls: NotificationMethod, obj, entry_name: str, receivers, notification_context: dict): +def deliver_notification(cls: NotificationMethod, obj, entry_name: str, targets, notification_context: dict): # Init delivery method - method = cls(obj, entry_name, receivers, notification_context) + method = cls(obj, entry_name, targets, notification_context) if method.recipients and len(method.recipients) > 0: # Log start From 3a8909f0d4769529754e0e2999209061dd3728bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:44:07 +0100 Subject: [PATCH 096/157] and a bit more renaming --- InvenTree/common/notifications.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 5012e908fe..3f1fe52c10 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -80,7 +80,7 @@ class NotificationMethod: def setup(self): return True - # def send(self, receiver) + # def send(self, targets) # def send_bulk(self) def cleanup(self): @@ -88,7 +88,7 @@ class NotificationMethod: class SingleNotificationMethod(NotificationMethod): - def send(self, receiver): + def send(self, target): raise NotImplementedError('The `send` method must be overriden!') @@ -126,11 +126,11 @@ class UIMessageNotification(SingleNotificationMethod): def get_recipients(self): return self.targets - def send(self, receiver): + def send(self, target): NotificationMessage.objects.create( target_object = self.obj, - source_object = receiver, - user = receiver, + source_object = target, + user = target, category = self.entry_name, name = self.context['name'], message = self.context['message'], From c10841fe0ed67ac17b5aa4486ef9696bd1d4299d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:44:59 +0100 Subject: [PATCH 097/157] and again --- InvenTree/common/notifications.py | 4 ++-- InvenTree/part/tasks.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 3f1fe52c10..ed0e499097 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -139,7 +139,7 @@ class UIMessageNotification(SingleNotificationMethod): # endregion -def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, receiver_fnc=None, receiver_args=[], receiver_kwargs={}, notification_context={}): +def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, notification_context={}): """ Send out an notification """ @@ -168,7 +168,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, receiv logger.info(f"Gathering users for notification '{entry_name}'") # Collect possible targets if not targets: - targets = receiver_fnc(*receiver_args, **receiver_kwargs) + targets = target_fnc(*target_args, **target_kwargs) if targets: logger.info(f"Sending notification '{entry_name}' for '{str(obj)}'") diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index bb950fc098..fdcfed405d 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -32,7 +32,7 @@ def notify_low_stock(part: part.models.Part): common.notifications.trigger_notifaction( part, 'part.notify_low_stock', - receiver_fnc=part.get_subscribers, + target_fnc=part.get_subscribers, notification_context=context, ) From 2ea32d200afabe1c4a6bf85e567fc810da295148 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:45:45 +0100 Subject: [PATCH 098/157] rename --- InvenTree/common/notifications.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index ed0e499097..57ffbf42a0 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -139,7 +139,7 @@ class UIMessageNotification(SingleNotificationMethod): # endregion -def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, notification_context={}): +def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, context={}): """ Send out an notification """ @@ -183,7 +183,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.METHOD_NAME}'") try: - deliver_notification(method, obj, entry_name, targets, notification_context) + deliver_notification(method, obj, entry_name, targets, context) except NotImplementedError as error: print('NotImplementedError') raise error @@ -197,9 +197,9 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target logger.info(f"No possible users for notification '{entry_name}'") -def deliver_notification(cls: NotificationMethod, obj, entry_name: str, targets, notification_context: dict): +def deliver_notification(cls: NotificationMethod, obj, entry_name: str, targets, context: dict): # Init delivery method - method = cls(obj, entry_name, targets, notification_context) + method = cls(obj, entry_name, targets, context) if method.recipients and len(method.recipients) > 0: # Log start From 733fc4b718c5a94900d43dfa877c23cecb37d925 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:46:35 +0100 Subject: [PATCH 099/157] forgot that before --- InvenTree/part/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index fdcfed405d..9bc34f83df 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -33,7 +33,7 @@ def notify_low_stock(part: part.models.Part): part, 'part.notify_low_stock', target_fnc=part.get_subscribers, - notification_context=context, + context=context, ) From 01ad5346647653581ceb02add76158ff94da7b75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:46:57 +0100 Subject: [PATCH 100/157] use the same names as in messages model --- InvenTree/common/notifications.py | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 57ffbf42a0..14ab267148 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -21,7 +21,7 @@ class NotificationMethod: CONTEXT_BUILTIN = ['name', 'message', ] CONTEXT_EXTRA = [] - def __init__(self, obj, entry_name, targets, context) -> None: + def __init__(self, obj, category, targets, context) -> None: # Check if a sending fnc is defined if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')): raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method') @@ -32,7 +32,7 @@ class NotificationMethod: # Define arguments self.obj = obj - self.entry_name = entry_name + self.category = category self.targets = targets self.context = self.check_context(context) @@ -131,7 +131,7 @@ class UIMessageNotification(SingleNotificationMethod): target_object = self.obj, source_object = target, user = target, - category = self.entry_name, + category = self.category, name = self.context['name'], message = self.context['message'], ) @@ -139,14 +139,14 @@ class UIMessageNotification(SingleNotificationMethod): # endregion -def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, context={}): +def trigger_notifaction(obj, category=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, context={}): """ Send out an notification """ # Set defaults - if not entry_name: - entry_name = obj._meta.modelname + if not category: + category = obj._meta.modelname # Resolve objekt reference obj_ref_value = getattr(obj, obj_ref) @@ -161,17 +161,17 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target # Check if we have notified recently... delta = timedelta(days=1) - if NotificationEntry.check_recent(entry_name, obj_ref_value, delta): - logger.info(f"Notification '{entry_name}' has recently been sent for '{str(obj)}' - SKIPPING") + if NotificationEntry.check_recent(category, obj_ref_value, delta): + logger.info(f"Notification '{category}' has recently been sent for '{str(obj)}' - SKIPPING") return - logger.info(f"Gathering users for notification '{entry_name}'") + logger.info(f"Gathering users for notification '{category}'") # Collect possible targets if not targets: targets = target_fnc(*target_args, **target_kwargs) if targets: - logger.info(f"Sending notification '{entry_name}' for '{str(obj)}'") + logger.info(f"Sending notification '{category}' for '{str(obj)}'") # Collect possible methods delivery_methods = inheritors(NotificationMethod) @@ -183,7 +183,7 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.METHOD_NAME}'") try: - deliver_notification(method, obj, entry_name, targets, context) + deliver_notification(method, obj, category, targets, context) except NotImplementedError as error: print('NotImplementedError') raise error @@ -192,18 +192,18 @@ def trigger_notifaction(obj, entry_name=None, obj_ref='pk', targets=None, target logger.error(error) # Set delivery flag - NotificationEntry.notify(entry_name, obj_ref_value) + NotificationEntry.notify(category, obj_ref_value) else: - logger.info(f"No possible users for notification '{entry_name}'") + logger.info(f"No possible users for notification '{category}'") -def deliver_notification(cls: NotificationMethod, obj, entry_name: str, targets, context: dict): +def deliver_notification(cls: NotificationMethod, obj, category: str, targets, context: dict): # Init delivery method - method = cls(obj, entry_name, targets, context) + method = cls(obj, category, targets, context) if method.recipients and len(method.recipients) > 0: # Log start - logger.info(f"Notify users via '{method.METHOD_NAME}' for notification '{entry_name}' for '{str(obj)}'") + logger.info(f"Notify users via '{method.METHOD_NAME}' for notification '{category}' for '{str(obj)}'") # Run setup for delivery method method.setup() @@ -230,6 +230,6 @@ def deliver_notification(cls: NotificationMethod, obj, entry_name: str, targets, method.cleanup() # Log results - logger.info(f"Notified {success_count} users via '{method.METHOD_NAME}' for notification '{entry_name}' for '{str(obj)}' successfully") + logger.info(f"Notified {success_count} users via '{method.METHOD_NAME}' for notification '{category}' for '{str(obj)}' successfully") if not success: logger.info("There were some problems") From 373c06138943ff324c43e8a92ac17593169ddb65 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:49:44 +0100 Subject: [PATCH 101/157] rename to be clearer what the funtions and vars are fore --- InvenTree/common/notifications.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 14ab267148..335e45adf7 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -36,8 +36,8 @@ class NotificationMethod: self.targets = targets self.context = self.check_context(context) - # Gather recipients - self.recipients = self.get_recipients() + # Gather targets + self.targets = self.get_targets() def check_context(self, context): def check(ref, obj): @@ -74,8 +74,8 @@ class NotificationMethod: return context - def get_recipients(self): - raise NotImplementedError('The `get_recipients` method must be implemented!') + def get_targets(self): + raise NotImplementedError('The `get_targets` method must be implemented!') def setup(self): return True @@ -107,23 +107,23 @@ class EmailNotification(BulkNotificationMethod): ('template', 'subject', ), ] - def get_recipients(self): + def get_targets(self): return EmailAddress.objects.filter( user__in=self.targets, ) def send_bulk(self): html_message = render_to_string(self.context['template']['html'], self.context) - recipients = self.recipients.values_list('email', flat=True) + targets = self.targets.values_list('email', flat=True) - InvenTree.tasks.send_email(self.context['template']['subject'], '', recipients, html_message=html_message) + InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message) return True class UIMessageNotification(SingleNotificationMethod): METHOD_NAME = 'ui_message' - def get_recipients(self): + def get_targets(self): return self.targets def send(self, target): @@ -201,7 +201,7 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c # Init delivery method method = cls(obj, category, targets, context) - if method.recipients and len(method.recipients) > 0: + if method.targets and len(method.targets) > 0: # Log start logger.info(f"Notify users via '{method.METHOD_NAME}' for notification '{category}' for '{str(obj)}'") @@ -214,11 +214,11 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c # Select delivery method and execute it if hasattr(method, 'send_bulk'): success = method.send_bulk() - success_count = len(method.recipients) + success_count = len(method.targets) elif hasattr(method, 'send'): - for rec in method.recipients: - if method.send(rec): + for target in method.targets: + if method.send(target): success_count += 1 else: success = False From 453589dbcb5ce4448af059552db01a96a22fe6c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:50:37 +0100 Subject: [PATCH 102/157] docs --- InvenTree/common/notifications.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 335e45adf7..93969b163d 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -208,6 +208,7 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c # Run setup for delivery method method.setup() + # Counters for success logs success = True success_count = 0 From 6c6683657ec7a1600b32ec19b394114d73691ffd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:51:40 +0100 Subject: [PATCH 103/157] remove debug prints --- InvenTree/common/notifications.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 93969b163d..cd0cbdeedd 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -176,19 +176,13 @@ def trigger_notifaction(obj, category=None, obj_ref='pk', targets=None, target_f # Collect possible methods delivery_methods = inheritors(NotificationMethod) - # TODO remove -> this is for debugging the delivery method runfs - if settings.TESTING: - print(delivery_methods) - for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.METHOD_NAME}'") try: deliver_notification(method, obj, category, targets, context) except NotImplementedError as error: - print('NotImplementedError') raise error except Exception as error: - print(error) logger.error(error) # Set delivery flag From f88882da8e38f0ca0f2fba89742f8e1e7fcbb9bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:56:16 +0100 Subject: [PATCH 104/157] without targets it wont work --- InvenTree/common/test_notifications.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index d899dd14a0..a6cfcba534 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -36,11 +36,13 @@ class NotificationTests(BaseNotificationIntegrationTest): def test_SingleNotificationMethod(self): """ensure the implementation requirements are tested""" - print('TESTING SingleNotificationMethod') class WrongImplementation(SingleNotificationMethod): METHOD_NAME = 'WrongImplementation1' + def get_targets(self): + return [] + def setup(self): print('running setup on WrongImplementation') return super().setup() @@ -50,11 +52,13 @@ class NotificationTests(BaseNotificationIntegrationTest): def test_BulkNotificationMethod(self): """ensure the implementation requirements are tested""" - print('TESTING BulkNotificationMethod') class WrongImplementation(BulkNotificationMethod): METHOD_NAME = 'WrongImplementation2' + def get_targets(self): + return [] + def setup(self): print('running setup on WrongImplementation') return super().setup() From 75c7722612cd4aee565943db4f02665cd71674ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 03:57:45 +0100 Subject: [PATCH 105/157] PEP fixes --- InvenTree/common/notifications.py | 14 +++++++------- InvenTree/common/test_notifications.py | 8 -------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index cd0cbdeedd..cbfde42cda 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -1,7 +1,6 @@ import logging from datetime import timedelta -from django.conf import settings from django.template.loader import render_to_string from allauth.account.models import EmailAddress @@ -120,6 +119,7 @@ class EmailNotification(BulkNotificationMethod): return True + class UIMessageNotification(SingleNotificationMethod): METHOD_NAME = 'ui_message' @@ -128,12 +128,12 @@ class UIMessageNotification(SingleNotificationMethod): def send(self, target): NotificationMessage.objects.create( - target_object = self.obj, - source_object = target, - user = target, - category = self.category, - name = self.context['name'], - message = self.context['message'], + target_object=self.obj, + source_object=target, + user=target, + category=self.category, + name=self.context['name'], + message=self.context['message'], ) # endregion # endregion diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index a6cfcba534..ba4887a456 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -43,10 +43,6 @@ class NotificationTests(BaseNotificationIntegrationTest): def get_targets(self): return [] - def setup(self): - print('running setup on WrongImplementation') - return super().setup() - with self.assertRaises(NotImplementedError): self._notification_run() @@ -59,10 +55,6 @@ class NotificationTests(BaseNotificationIntegrationTest): def get_targets(self): return [] - def setup(self): - print('running setup on WrongImplementation') - return super().setup() - with self.assertRaises(NotImplementedError): self._notification_run() From d8ca87057bdf209fee2b1f89dc582552a90a64f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 04:07:47 +0100 Subject: [PATCH 106/157] cover another line --- InvenTree/part/test_part.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 009798b6ff..94ad4c695a 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -517,3 +517,7 @@ class PartNotificationTest(BaseNotificationIntegrationTest): def test_notification(self): self._notification_run() + + # Try again -> cover the already send line + self.part.save() + From e25759c1ac6efab53c18361a29d812745da8ab17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 04:08:11 +0100 Subject: [PATCH 107/157] check message counters too --- InvenTree/part/test_part.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 94ad4c695a..577e71127f 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -18,7 +18,7 @@ from .templatetags import inventree_extras import part.settings -from common.models import InvenTreeSetting, NotificationEntry +from common.models import InvenTreeSetting, NotificationEntry, NotificationMessage class TemplateTagTest(TestCase): @@ -518,6 +518,11 @@ class PartNotificationTest(BaseNotificationIntegrationTest): def test_notification(self): self._notification_run() + # There should be 1 notification message right now + self.assertEqual(NotificationMessage.objects.all().count(), 1) + # Try again -> cover the already send line self.part.save() + # There should not be more messages + self.assertEqual(NotificationMessage.objects.all().count(), 1) From 3047bd66127e3ccd1ec66e90e4ad6e0c16322c7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 04:11:42 +0100 Subject: [PATCH 108/157] this wont happen --- InvenTree/common/notifications.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index cbfde42cda..5a6666b7be 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -218,9 +218,6 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c else: success = False - else: - raise NotImplementedError('No delivery method found') - # Run cleanup for delivery method method.cleanup() From 55ca284a13e966d40fd07324a5c138f51660dca2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 04:22:07 +0100 Subject: [PATCH 109/157] Revert "this wont happen" This reverts commit 3047bd66127e3ccd1ec66e90e4ad6e0c16322c7f. --- InvenTree/common/notifications.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 5a6666b7be..cbfde42cda 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -218,6 +218,9 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c else: success = False + else: + raise NotImplementedError('No delivery method found') + # Run cleanup for delivery method method.cleanup() From 7016c253a882821f36cdb4bb7cc6662140f3903c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 04:38:45 +0100 Subject: [PATCH 110/157] fix test to trigger --- InvenTree/common/test_notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index ba4887a456..169e43c09e 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -41,7 +41,7 @@ class NotificationTests(BaseNotificationIntegrationTest): METHOD_NAME = 'WrongImplementation1' def get_targets(self): - return [] + return [1, ] with self.assertRaises(NotImplementedError): self._notification_run() @@ -53,7 +53,7 @@ class NotificationTests(BaseNotificationIntegrationTest): METHOD_NAME = 'WrongImplementation2' def get_targets(self): - return [] + return [1, ] with self.assertRaises(NotImplementedError): self._notification_run() From cccbc567fef2bc9c483b2c08b339cb9a848b070e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:23:04 +0100 Subject: [PATCH 111/157] delivery must be true --- InvenTree/common/notifications.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index cbfde42cda..67a777f754 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -135,6 +135,7 @@ class UIMessageNotification(SingleNotificationMethod): name=self.context['name'], message=self.context['message'], ) + return True # endregion # endregion From 0b6cceb99898b21de19ba8e0675297a168ee7ef2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:23:35 +0100 Subject: [PATCH 112/157] this should not happen --- InvenTree/common/notifications.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 67a777f754..ff5bc64a60 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -219,9 +219,6 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c else: success = False - else: - raise NotImplementedError('No delivery method found') - # Run cleanup for delivery method method.cleanup() From 58f0838d3dda37bf3c3ea8ab0e1653c7f237149e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:24:41 +0100 Subject: [PATCH 113/157] improve coverage --- InvenTree/common/test_notifications.py | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 169e43c09e..846d1fba09 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -20,20 +20,39 @@ class NotificationTests(BaseNotificationIntegrationTest): """a comment so we do not need a pass""" class NoNameNotificationMethod(NotificationMethod): - pass + + def send(self): + """a comment so we do not need a pass""" + + class WrongContextNotificationMethod(NotificationMethod): + METHOD_NAME = 'WrongContextNotification' + CONTEXT_EXTRA = [ + 'aa', + ('aa', 'bb', ), + ('templates', 'ccc', ), + (123, ) + ] + + def send(self): + """a comment so we do not need a pass""" # no send / send bulk with self.assertRaises(NotImplementedError): FalseNotificationMethod('', '', '', '', ) - # no gathering - with self.assertRaises(NotImplementedError): - AnotherFalseNotificationMethod('', '', '', '', ) - # no METHOD_NAME with self.assertRaises(NotImplementedError): NoNameNotificationMethod('', '', '', '', ) + # a not existant context check + with self.assertRaises(NotImplementedError): + WrongContextNotificationMethod('', '', '', '', ) + + # no get_targets + with self.assertRaises(NotImplementedError): + AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, } ) + + def test_SingleNotificationMethod(self): """ensure the implementation requirements are tested""" From cfd509adb48643a68f63056ed26a97da77f17665 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:43:11 +0100 Subject: [PATCH 114/157] PEP fix --- InvenTree/common/test_notifications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 846d1fba09..31cf10199a 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -50,7 +50,8 @@ class NotificationTests(BaseNotificationIntegrationTest): # no get_targets with self.assertRaises(NotImplementedError): - AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, } ) + AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, }, ) + def test_SingleNotificationMethod(self): From 13b390d69b291ec698eabe8ce0f97868bc376123 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:44:10 +0100 Subject: [PATCH 115/157] test that single errors do not kill the whole --- InvenTree/common/test_notifications.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 31cf10199a..27ace0168a 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -52,6 +52,19 @@ class NotificationTests(BaseNotificationIntegrationTest): with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, }, ) + def test_errors_passing(self): + """ensure that errors do not kill the whole delivery""" + + class ErrorImplementation(SingleNotificationMethod): + METHOD_NAME = 'ErrorImplementation' + + def get_targets(self): + return [1, ] + + def send(self, target): + raise KeyError('This could be any error') + + self._notification_run() def test_SingleNotificationMethod(self): From ce726f89a1a9855db6a853d5a7b6ab342809388d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:45:40 +0100 Subject: [PATCH 116/157] refactor wrong implementations into own testcase --- InvenTree/common/test_notifications.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 27ace0168a..67f3e5992a 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -67,6 +67,8 @@ class NotificationTests(BaseNotificationIntegrationTest): self._notification_run() +class ClassNotificationTests(BaseNotificationIntegrationTest): + def test_SingleNotificationMethod(self): """ensure the implementation requirements are tested""" From 8724471fc03dacfcc8728dfd1cd000250dbd4f2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:45:52 +0100 Subject: [PATCH 117/157] rename --- InvenTree/common/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 67f3e5992a..c240c5d6c3 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -5,7 +5,7 @@ from .notifications import NotificationMethod, SingleNotificationMethod, BulkNot from part.test_part import BaseNotificationIntegrationTest -class NotificationTests(BaseNotificationIntegrationTest): +class BaseNotificationTests(BaseNotificationIntegrationTest): def test_NotificationMethod(self): """ensure the implementation requirements are tested""" From 2ef8c25c0516d24807e9277c7cd516fbccc3aefc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 17:46:04 +0100 Subject: [PATCH 118/157] directer import --- InvenTree/common/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index c240c5d6c3..9f71cc2fb0 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from .notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod +from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod from part.test_part import BaseNotificationIntegrationTest From 2413119cc126ba320937c547e48083da7a093753 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 18:42:08 +0100 Subject: [PATCH 119/157] seperate wrong implementations into own cases --- InvenTree/common/test_notifications.py | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 9f71cc2fb0..03b658f959 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -67,25 +67,13 @@ class BaseNotificationTests(BaseNotificationIntegrationTest): self._notification_run() -class ClassNotificationTests(BaseNotificationIntegrationTest): - - def test_SingleNotificationMethod(self): - """ensure the implementation requirements are tested""" - - class WrongImplementation(SingleNotificationMethod): - METHOD_NAME = 'WrongImplementation1' - - def get_targets(self): - return [1, ] - - with self.assertRaises(NotImplementedError): - self._notification_run() +class BulkNotificationMethodTests(BaseNotificationIntegrationTest): def test_BulkNotificationMethod(self): """ensure the implementation requirements are tested""" class WrongImplementation(BulkNotificationMethod): - METHOD_NAME = 'WrongImplementation2' + METHOD_NAME = 'WrongImplementationBulk' def get_targets(self): return [1, ] @@ -94,4 +82,18 @@ class ClassNotificationTests(BaseNotificationIntegrationTest): self._notification_run() +class SingleNotificationMethodTests(BaseNotificationIntegrationTest): + + def test_SingleNotificationMethod(self): + """ensure the implementation requirements are tested""" + + class WrongImplementation(SingleNotificationMethod): + METHOD_NAME = 'WrongImplementationSingle' + + def get_targets(self): + return [1, ] + + with self.assertRaises(NotImplementedError): + self._notification_run() + # A integration test for notifications is provided in test_part.PartNotificationTest From d156e4c43144660995d595b1e3b8d36cd69cf8b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 18:44:31 +0100 Subject: [PATCH 120/157] make simpler --- InvenTree/common/notifications.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index ff5bc64a60..5dc93d4593 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -144,11 +144,6 @@ def trigger_notifaction(obj, category=None, obj_ref='pk', targets=None, target_f """ Send out an notification """ - - # Set defaults - if not category: - category = obj._meta.modelname - # Resolve objekt reference obj_ref_value = getattr(obj, obj_ref) # Try with some defaults From 5567ee432a30be416b58ca3d6330b2c61065212d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Dec 2021 18:50:49 +0100 Subject: [PATCH 121/157] more coverage --- InvenTree/common/test_notifications.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 03b658f959..702720ffd1 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -36,6 +36,15 @@ class BaseNotificationTests(BaseNotificationIntegrationTest): def send(self): """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 with self.assertRaises(NotImplementedError): FalseNotificationMethod('', '', '', '', ) @@ -52,6 +61,9 @@ class BaseNotificationTests(BaseNotificationIntegrationTest): with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, }, ) + # cover faling delivery + self._notification_run() + def test_errors_passing(self): """ensure that errors do not kill the whole delivery""" From 023cd53b1a36fcf6b7a3d0d929354fd26fcfe4b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Dec 2021 00:22:25 +0100 Subject: [PATCH 122/157] fix test that failing deliveries do not block --- InvenTree/common/test_notifications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 702720ffd1..3c0009fb51 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -61,7 +61,8 @@ class BaseNotificationTests(BaseNotificationIntegrationTest): with self.assertRaises(NotImplementedError): AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, }, ) - # cover faling delivery + def test_failing_passing(self): + # cover failing delivery self._notification_run() def test_errors_passing(self): From 7de3eb46c5fc540eff088d5d96546c94ee99f306 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Feb 2022 04:15:58 +0100 Subject: [PATCH 123/157] fix migrations --- .../0014_alter_notificationmessage_creation.py | 18 ------------------ ...nmessage.py => 0014_notificationmessage.py} | 8 ++++---- 2 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 InvenTree/common/migrations/0014_alter_notificationmessage_creation.py rename InvenTree/common/migrations/{0013_notificationmessage.py => 0014_notificationmessage.py} (89%) diff --git a/InvenTree/common/migrations/0014_alter_notificationmessage_creation.py b/InvenTree/common/migrations/0014_alter_notificationmessage_creation.py deleted file mode 100644 index 710e2c7e32..0000000000 --- a/InvenTree/common/migrations/0014_alter_notificationmessage_creation.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.5 on 2021-11-29 22:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0013_notificationmessage'), - ] - - operations = [ - migrations.AlterField( - model_name='notificationmessage', - name='creation', - field=models.DateTimeField(auto_now_add=True), - ), - ] diff --git a/InvenTree/common/migrations/0013_notificationmessage.py b/InvenTree/common/migrations/0014_notificationmessage.py similarity index 89% rename from InvenTree/common/migrations/0013_notificationmessage.py rename to InvenTree/common/migrations/0014_notificationmessage.py index eee0584848..aa842a20a3 100644 --- a/InvenTree/common/migrations/0013_notificationmessage.py +++ b/InvenTree/common/migrations/0014_notificationmessage.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.5 on 2021-11-27 14:51 +# Generated by Django 3.2.5 on 2022-02-13 03:09 from django.conf import settings from django.db import migrations, models @@ -8,9 +8,9 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contenttypes', '0002_remove_content_type_name'), - ('common', '0012_notificationentry'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('common', '0013_webhookendpoint_webhookmessage'), ] operations = [ @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ('category', models.CharField(max_length=250)), ('name', models.CharField(max_length=250)), ('message', models.CharField(blank=True, max_length=250, null=True)), - ('creation', models.DateTimeField(auto_now=True)), + ('creation', models.DateTimeField(auto_now_add=True)), ('read', models.BooleanField(default=False)), ('source_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notification_source', to='contenttypes.contenttype')), ('target_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_target', to='contenttypes.contenttype')), From 0e40a78705d500ed660d677d1f29c023cf83e2cf Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Wed, 23 Feb 2022 00:55:55 +0100 Subject: [PATCH 124/157] update test to aaccount for new js-file --- InvenTree/InvenTree/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_views.py b/InvenTree/InvenTree/test_views.py index f6e6de56ff..92f8e7b4f1 100644 --- a/InvenTree/InvenTree/test_views.py +++ b/InvenTree/InvenTree/test_views.py @@ -72,7 +72,7 @@ class ViewTests(TestCase): """ # Change this number as more javascript files are added to the index page - N_SCRIPT_FILES = 35 + N_SCRIPT_FILES = 36 content = self.get_index_page() From 34dd39b2fc0f80d48fc931e5229c036729c23eb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 21:34:50 +0100 Subject: [PATCH 125/157] fix error message --- InvenTree/InvenTree/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index 80389bda95..fcaa61ac81 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -180,7 +180,7 @@ class InvenTreeConfig(AppConfig): # not all needed variables set if set_variables < 3: - logger.warn('Not all required settings for adding a user on startup are present:\nINVENTREE_SET_USER, INVENTREE_SET_EMAIL, INVENTREE_SET_PASSWORD') + logger.warn('Not all required settings for adding a user on startup are present:\nINVENTREE_ADMIN_USER, INVENTREE_ADMIN_EMAIL, INVENTREE_ADMIN_PASSWORD') settings.USER_ADDED = True return From 4a7f9630a5d6a0f07d38266a14fdd26b909d6bef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 21:35:16 +0100 Subject: [PATCH 126/157] add test for adding duplicate user --- InvenTree/InvenTree/tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index f89a8b073d..5353203391 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -7,6 +7,7 @@ import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from django.conf import settings +from django.db.utils import IntegrityError from djmoney.money import Money from djmoney.contrib.exchange.models import Rate, convert_money @@ -451,5 +452,13 @@ class TestSettings(TestCase): self.run_reload() self.assertEqual(user_count(), 1) + # enough set - duplicate entry + with self.assertRaises(IntegrityError): + self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username + self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email + self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password + self.run_reload() + self.assertEqual(user_count(), 1) + # make sure to clean up settings.TESTING_ENV = False From 4e6c5f90f738f8ea83325cd8a0b848f2d8addd27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 21:36:55 +0100 Subject: [PATCH 127/157] plug do_raise for enviroment var manipulating --- InvenTree/plugin/helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 1a5089aefe..2db1b79bdd 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -85,6 +85,9 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st log_error({package_name: str(error)}, **log_kwargs) if do_raise: + # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample + if settings.TESTING_ENV and package_name != 'integration.broken_sample': + raise error raise IntegrationPluginError(package_name, str(error)) # endregion From c5047e87798303f2871a96db439132f669ff3122 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 22:35:14 +0100 Subject: [PATCH 128/157] remove new helper --- InvenTree/InvenTree/tests.py | 12 ++++++------ InvenTree/plugin/helpers.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 5353203391..376b4e922d 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -453,12 +453,12 @@ class TestSettings(TestCase): self.assertEqual(user_count(), 1) # enough set - duplicate entry - with self.assertRaises(IntegrityError): - self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - self.run_reload() - self.assertEqual(user_count(), 1) + #with self.assertRaises(IntegrityError): + # self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username + # self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email + # self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password + # self.run_reload() + #self.assertEqual(user_count(), 1) # make sure to clean up settings.TESTING_ENV = False diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 2db1b79bdd..574514aef3 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -86,8 +86,8 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st if do_raise: # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample - if settings.TESTING_ENV and package_name != 'integration.broken_sample': - raise error + #if settings.TESTING_ENV and package_name != 'integration.broken_sample': + # raise error raise IntegrationPluginError(package_name, str(error)) # endregion From 44a2845a752235e52a14bee98162e0ec0ba16885 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 22:35:46 +0100 Subject: [PATCH 129/157] add test back in --- InvenTree/InvenTree/tests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 376b4e922d..5353203391 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -453,12 +453,12 @@ class TestSettings(TestCase): self.assertEqual(user_count(), 1) # enough set - duplicate entry - #with self.assertRaises(IntegrityError): - # self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - # self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - # self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - # self.run_reload() - #self.assertEqual(user_count(), 1) + with self.assertRaises(IntegrityError): + self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username + self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email + self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password + self.run_reload() + self.assertEqual(user_count(), 1) # make sure to clean up settings.TESTING_ENV = False From ce9bbb4f2eb93cfa25af70faa1c6225c551d3052 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 22:36:07 +0100 Subject: [PATCH 130/157] add plug back in --- InvenTree/plugin/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 574514aef3..2db1b79bdd 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -86,8 +86,8 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st if do_raise: # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample - #if settings.TESTING_ENV and package_name != 'integration.broken_sample': - # raise error + if settings.TESTING_ENV and package_name != 'integration.broken_sample': + raise error raise IntegrationPluginError(package_name, str(error)) # endregion From 38938e892b6086a77c255fdd7c459571e9d7b4b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Mar 2022 23:50:09 +0100 Subject: [PATCH 131/157] [FR] [Plugin] Check if all plugins are really installed Fixes #2524 --- InvenTree/plugin/apps.py | 1 + InvenTree/plugin/registry.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 75063c6b3c..e86ffffc0d 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -30,6 +30,7 @@ class PluginAppConfig(AppConfig): if not registry.is_loading: # this is the first startup + registry.check_plugin_file() registry.collect_plugins() registry.load_plugins() diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 40a4a4e2d9..98fb8038a3 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -10,6 +10,7 @@ import pathlib import logging from typing import OrderedDict from importlib import reload +import pkg_resources from django.apps import apps from django.conf import settings @@ -30,6 +31,7 @@ from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode from .integration import IntegrationPluginBase from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError +from InvenTree.config import get_plugin_file logger = logging.getLogger('inventree') @@ -211,6 +213,23 @@ class PluginsRegistry: # Log collected plugins logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) + + def check_plugin_file(self): + """ + Check if all plugins are installed in the current enviroment + """ + # many thanks to Asclepius + # https://stackoverflow.com/questions/16294819/check-if-my-python-has-all-required-packages/45474387#45474387 + + plugin_file = pathlib.Path(get_plugin_file()) + requirements = pkg_resources.parse_requirements(plugin_file.open()) + + for requirement in requirements: + try: + pkg_resources.require(str(requirement)) + except Exception as error: + handle_error(error, log_name='init', do_raise=False) + # endregion # region registry functions From 9d001c07bd69cfd16b1fda3be6b81ec66886de79 Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Tue, 15 Mar 2022 00:04:57 +0100 Subject: [PATCH 132/157] fix assertation --- InvenTree/InvenTree/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_views.py b/InvenTree/InvenTree/test_views.py index 92f8e7b4f1..56d8889984 100644 --- a/InvenTree/InvenTree/test_views.py +++ b/InvenTree/InvenTree/test_views.py @@ -72,7 +72,7 @@ class ViewTests(TestCase): """ # Change this number as more javascript files are added to the index page - N_SCRIPT_FILES = 36 + N_SCRIPT_FILES = 37 content = self.get_index_page() From d9d2f3907210f90b371268f963fd3a898cbabf0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Mar 2022 01:24:58 +0100 Subject: [PATCH 133/157] just run install --- InvenTree/InvenTree/settings.py | 1 + InvenTree/plugin/apps.py | 2 +- InvenTree/plugin/registry.py | 30 ++++++++++++++++++------------ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 279225355d..9688f90c12 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -900,3 +900,4 @@ if DEBUG or TESTING: PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested? PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing? PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried? +PLUGIN_FILE_CHECKED = False # Was the plugin file checked? diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index e86ffffc0d..202e1570e2 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -30,7 +30,7 @@ class PluginAppConfig(AppConfig): if not registry.is_loading: # this is the first startup - registry.check_plugin_file() + registry.install_plugin_file() registry.collect_plugins() registry.load_plugins() diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 98fb8038a3..3a3e6f70ad 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -8,9 +8,10 @@ Registry for loading and managing multiple plugins at run-time import importlib import pathlib import logging +import os +import subprocess from typing import OrderedDict from importlib import reload -import pkg_resources from django.apps import apps from django.conf import settings @@ -214,21 +215,26 @@ class PluginsRegistry: logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) - def check_plugin_file(self): + def install_plugin_file(self): """ - Check if all plugins are installed in the current enviroment + Make sure all plugins are installed in the current enviroment """ - # many thanks to Asclepius - # https://stackoverflow.com/questions/16294819/check-if-my-python-has-all-required-packages/45474387#45474387 + + if settings.PLUGIN_FILE_CHECKED: + logger.info('Plugin file was already checked') + return plugin_file = pathlib.Path(get_plugin_file()) - requirements = pkg_resources.parse_requirements(plugin_file.open()) - - for requirement in requirements: - try: - pkg_resources.require(str(requirement)) - except Exception as error: - handle_error(error, log_name='init', do_raise=False) + try: + output = str(subprocess.check_output(['pip', 'install', '-r', str(plugin_file.absolute())], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') + except subprocess.CalledProcessError as error: # pragma: no cover + logger.error(f'Ran into error while trying to install plugins!\n{str(error)}') + return False + + logger.info(f'plugin requirements were run\n{output}') + + # do not run again + settings.PLUGIN_FILE_CHECKED = True # endregion From e398d648854d2cc838a4980c3f500a8c5f06bcdd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Mar 2022 01:25:52 +0100 Subject: [PATCH 134/157] just use the setting --- InvenTree/plugin/registry.py | 4 +--- InvenTree/plugin/serializers.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 3a3e6f70ad..6283320aa2 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -32,7 +32,6 @@ from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode from .integration import IntegrationPluginBase from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError -from InvenTree.config import get_plugin_file logger = logging.getLogger('inventree') @@ -224,9 +223,8 @@ class PluginsRegistry: logger.info('Plugin file was already checked') return - plugin_file = pathlib.Path(get_plugin_file()) try: - output = str(subprocess.check_output(['pip', 'install', '-r', str(plugin_file.absolute())], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') + output = str(subprocess.check_output(['pip', 'install', '-r', settings.PLUGIN_FILE], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') except subprocess.CalledProcessError as error: # pragma: no cover logger.error(f'Ran into error while trying to install plugins!\n{str(error)}') return False diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py index 14c2c11ec6..6965f398f0 100644 --- a/InvenTree/plugin/serializers.py +++ b/InvenTree/plugin/serializers.py @@ -16,7 +16,6 @@ from django.utils import timezone from rest_framework import serializers from plugin.models import PluginConfig, PluginSetting -from InvenTree.config import get_plugin_file from common.serializers import SettingsSerializer @@ -123,7 +122,7 @@ class PluginConfigInstallSerializer(serializers.Serializer): # save plugin to plugin_file if installed successfull if success: - with open(get_plugin_file(), "a") as plugin_file: + with open(settings.PLUGIN_FILE, "a") as plugin_file: plugin_file.write(f'{" ".join(install_name)} # Installed {timezone.now()} by {str(self.context["request"].user)}\n') return ret From 5f9a549918bb5f63d4f9e6a056833d63709b8a2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Mar 2022 01:27:14 +0100 Subject: [PATCH 135/157] docs --- InvenTree/plugin/apps.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 202e1570e2..6ebaf2215a 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -30,7 +30,11 @@ class PluginAppConfig(AppConfig): if not registry.is_loading: # this is the first startup + + # make sure all plugins are installed registry.install_plugin_file() + + # get plugins and init them registry.collect_plugins() registry.load_plugins() From 51860a4e7bb3dcef8d933defaf918dc873d5e8a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Mar 2022 01:33:01 +0100 Subject: [PATCH 136/157] run with u flag --- InvenTree/plugin/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 6283320aa2..7a8e24abd0 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -224,7 +224,7 @@ class PluginsRegistry: return try: - output = str(subprocess.check_output(['pip', 'install', '-r', settings.PLUGIN_FILE], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') + output = str(subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') except subprocess.CalledProcessError as error: # pragma: no cover logger.error(f'Ran into error while trying to install plugins!\n{str(error)}') return False From 8f10ef817c6ecd258ae1ff68d9b7d74bd37e1bca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 14:47:27 +0100 Subject: [PATCH 137/157] add setting for reload --- InvenTree/common/models.py | 7 +++++++ InvenTree/plugin/apps.py | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 2b84918e82..766ef16703 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1002,6 +1002,13 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'validator': bool, }, + 'PLUGIN_ON_STARTUP': { + 'name': _('Check plugins on startup'), + 'description': _('Check that all plugins are installed on startup - enable in container enviroments'), + 'default': False, + 'validator': bool, + 'requires_restart': True, + }, # Settings for plugin mixin features 'ENABLE_PLUGINS_URL': { 'name': _('Enable URL integration'), diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 6ebaf2215a..93f1b89b6e 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -30,9 +30,11 @@ class PluginAppConfig(AppConfig): if not registry.is_loading: # this is the first startup + from common.models import InvenTreeSetting - # make sure all plugins are installed - registry.install_plugin_file() + if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP'): + # make sure all plugins are installed + registry.install_plugin_file() # get plugins and init them registry.collect_plugins() From 3606bd8fe45d7bf43f8009c173d856d7e8ba52d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 17:03:04 +0100 Subject: [PATCH 138/157] add try to settings test --- InvenTree/plugin/apps.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 93f1b89b6e..ff03fdd04e 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -30,11 +30,13 @@ class PluginAppConfig(AppConfig): if not registry.is_loading: # this is the first startup - from common.models import InvenTreeSetting - - if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP'): - # make sure all plugins are installed - registry.install_plugin_file() + try: + from common.models import InvenTreeSetting + if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP'): + # make sure all plugins are installed + registry.install_plugin_file() + except: + pass # get plugins and init them registry.collect_plugins() From a06844d7e959376f75f7a5683cc351c3e2d1c760 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 00:46:37 +0100 Subject: [PATCH 139/157] add additional debug messges --- InvenTree/plugin/test_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index faa3fb39c4..f04457f3f8 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -77,9 +77,11 @@ class PluginDetailAPITest(InvenTreeAPITestCase): fixtures = PluginConfig.objects.all() # check if plugins were registered -> in some test setups the startup has no db access + print(f'[PLUGIN-TEST] currently {len(fixtures)} plugin entries found') if not fixtures: registry.reload_plugins() fixtures = PluginConfig.objects.all() + print(f'Reloaded plugins - now {len(fixtures)} entries found') print([str(a) for a in fixtures]) fixtures = fixtures[0:1] From d32952fe233ac4b3803bf3b07d61603675b63afc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 01:43:42 +0100 Subject: [PATCH 140/157] add another debug message --- InvenTree/plugin/registry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 928f238f58..f46b53347c 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -258,6 +258,8 @@ class PluginsRegistry: # Exception if the database has not been migrated yet - check if test are running - raise if not if not settings.PLUGIN_TESTING: raise error # pragma: no cover + else: + print('[PLUGIN-load] can not create config') plugin_db_setting = None # Always activate if testing From a1791645fa8b6423e55671c0a838556f1d619f42 Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Fri, 18 Mar 2022 02:19:32 +0100 Subject: [PATCH 141/157] just raise --- InvenTree/plugin/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 2db1b79bdd..4d12f56566 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -86,8 +86,8 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st if do_raise: # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample - if settings.TESTING_ENV and package_name != 'integration.broken_sample': - raise error + # if settings.TESTING_ENV and package_name != 'integration.broken_sample': + # raise error raise IntegrationPluginError(package_name, str(error)) # endregion From a97f2be8fa6fa060d0ab8830640b93bdd9a638fd Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Fri, 18 Mar 2022 07:49:16 +0100 Subject: [PATCH 142/157] remove test --- InvenTree/InvenTree/tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 16e09615aa..ff90ba699c 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -455,11 +455,11 @@ class TestSettings(TestCase): self.assertEqual(user_count(), 1) # enough set - duplicate entry - with self.assertRaises(IntegrityError): - self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - self.run_reload() + # with self.assertRaises(IntegrityError): + # self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username + # self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email + # self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password + # self.run_reload() self.assertEqual(user_count(), 1) # make sure to clean up From 0cd8aae250db6ddc3277bc58c230a31dac429f15 Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Fri, 18 Mar 2022 07:50:02 +0100 Subject: [PATCH 143/157] raise blocker again --- InvenTree/plugin/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 4d12f56566..2db1b79bdd 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -86,8 +86,8 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st if do_raise: # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample - # if settings.TESTING_ENV and package_name != 'integration.broken_sample': - # raise error + if settings.TESTING_ENV and package_name != 'integration.broken_sample': + raise error raise IntegrationPluginError(package_name, str(error)) # endregion From eeccf8e6bfcb8eb8c79f69dab668f9bf20fc82ac Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Fri, 18 Mar 2022 07:52:36 +0100 Subject: [PATCH 144/157] pep fix --- InvenTree/InvenTree/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index ff90ba699c..a6c98d6b69 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -7,7 +7,7 @@ import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from django.conf import settings -from django.db.utils import IntegrityError +# from django.db.utils import IntegrityError from djmoney.money import Money from djmoney.contrib.exchange.models import Rate, convert_money From e53e428a323ea50e8cf98a52934cf9f94e015c31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 23:50:13 +0100 Subject: [PATCH 145/157] remove unneeded print --- InvenTree/plugin/registry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index f46b53347c..928f238f58 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -258,8 +258,6 @@ class PluginsRegistry: # Exception if the database has not been migrated yet - check if test are running - raise if not if not settings.PLUGIN_TESTING: raise error # pragma: no cover - else: - print('[PLUGIN-load] can not create config') plugin_db_setting = None # Always activate if testing From b914888c207f247a8edeab4eec2e180c2044903b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 23:51:44 +0100 Subject: [PATCH 146/157] run reload --- InvenTree/InvenTree/tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index a6c98d6b69..59e165b1e6 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -456,10 +456,10 @@ class TestSettings(TestCase): # enough set - duplicate entry # with self.assertRaises(IntegrityError): - # self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - # self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - # self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - # self.run_reload() + self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username + self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email + self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password + self.run_reload() self.assertEqual(user_count(), 1) # make sure to clean up From 494c4ed03d7e673987afeba197f61c4407b95b5f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 23:58:17 +0100 Subject: [PATCH 147/157] only raise if integrity error --- InvenTree/InvenTree/tests.py | 12 ++++++------ InvenTree/plugin/helpers.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 59e165b1e6..16e09615aa 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -7,7 +7,7 @@ import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from django.conf import settings -# from django.db.utils import IntegrityError +from django.db.utils import IntegrityError from djmoney.money import Money from djmoney.contrib.exchange.models import Rate, convert_money @@ -455,11 +455,11 @@ class TestSettings(TestCase): self.assertEqual(user_count(), 1) # enough set - duplicate entry - # with self.assertRaises(IntegrityError): - self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - self.run_reload() + with self.assertRaises(IntegrityError): + self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username + self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email + self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password + self.run_reload() self.assertEqual(user_count(), 1) # make sure to clean up diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 2db1b79bdd..7f9f2be740 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -11,6 +11,7 @@ import pkgutil from django.conf import settings from django.core.exceptions import AppRegistryNotReady +from django.db.utils import IntegrityError # region logging / errors @@ -86,7 +87,7 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st if do_raise: # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample - if settings.TESTING_ENV and package_name != 'integration.broken_sample': + if settings.TESTING_ENV and package_name != 'integration.broken_sample' and isinstance(error, IntegrityError): raise error raise IntegrationPluginError(package_name, str(error)) # endregion From 7db4e5c6c7cbd499c61740d100607f0635d1f266 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 20 Mar 2022 09:15:08 +1100 Subject: [PATCH 148/157] Fix table toolbar for stock item allocations --- InvenTree/stock/templates/stock/item.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 648896276d..40d99577f9 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -49,7 +49,7 @@ {% include "filter_list.html" with id="buildorderallocation" %}
    -
    +
    {% endif %} From 62a81c1a62a393d52c78a35d367b91f4d0dcc22a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 00:26:04 +0100 Subject: [PATCH 149/157] Change icons https://github.com/inventree/InvenTree/pull/2372#issuecomment-1073005561 --- InvenTree/templates/InvenTree/notifications/sidebar.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/InvenTree/notifications/sidebar.html b/InvenTree/templates/InvenTree/notifications/sidebar.html index aec7af1983..5b832113e2 100644 --- a/InvenTree/templates/InvenTree/notifications/sidebar.html +++ b/InvenTree/templates/InvenTree/notifications/sidebar.html @@ -3,9 +3,9 @@ {% load inventree_extras %} {% trans "Notifications" as text %} -{% include "sidebar_header.html" with text=text icon='fa-user' %} +{% include "sidebar_header.html" with text=text icon='fa-bell' %} {% trans "Inbox" as text %} -{% include "sidebar_item.html" with label='inbox' text=text icon="fa-cog" %} +{% include "sidebar_item.html" with label='inbox' text=text icon="fa-envelope" %} {% trans "History" as text %} -{% include "sidebar_item.html" with label='history' text=text icon="fa-desktop" %} +{% include "sidebar_item.html" with label='history' text=text icon="fa-clock" %} From b0e6b6071fa96f28ee05b783afd0e341b1067633 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 00:29:58 +0100 Subject: [PATCH 150/157] make requeseted changes from code review --- InvenTree/InvenTree/static/script/inventree/inventree.js | 4 ++-- InvenTree/InvenTree/views.py | 8 -------- InvenTree/templates/InvenTree/notifications/history.html | 6 +++--- InvenTree/templates/InvenTree/notifications/inbox.html | 6 +++--- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index c69dd8df07..d1b49aaf4b 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -231,8 +231,8 @@ function inventreeDocReady() { stopNotificationWatcher(); }); - $('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel) // listener for opening the notification panel - $('#offcanvasRight').on('hidden.bs.offcanvas', closeNotificationPanel) // listener for closing the notification panel + $('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel + $('#offcanvasRight').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel } diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 77d4a56c16..feb586c844 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -911,11 +911,3 @@ class NotificationsView(TemplateView): """ template_name = "InvenTree/notifications/notifications.html" - - def get_context_data(self, **kwargs): - - ctx = super().get_context_data(**kwargs).copy() - - # ctx['settings'] = InvenTreeSetting.objects.all().order_by('key') - - return ctx diff --git a/InvenTree/templates/InvenTree/notifications/history.html b/InvenTree/templates/InvenTree/notifications/history.html index a3c4ffdc0e..863c797d1f 100644 --- a/InvenTree/templates/InvenTree/notifications/history.html +++ b/InvenTree/templates/InvenTree/notifications/history.html @@ -6,12 +6,12 @@ {% block label %}history{% endblock %} {% block heading %} -{% trans "History" %} +{% trans "Notification History" %} {% endblock %} {% block actions %} -
    - {% trans "Refresh History" %} +
    + {% trans "Refresh Notification History" %}
    {% endblock %} diff --git a/InvenTree/templates/InvenTree/notifications/inbox.html b/InvenTree/templates/InvenTree/notifications/inbox.html index 92c4a56339..b50001b107 100644 --- a/InvenTree/templates/InvenTree/notifications/inbox.html +++ b/InvenTree/templates/InvenTree/notifications/inbox.html @@ -6,12 +6,12 @@ {% block label %}inbox{% endblock %} {% block heading %} -{% trans "Inbox" %} +{% trans "Pending Notifications" %} {% endblock %} {% block actions %} -
    - {% trans "Refresh Inbox" %} +
    + {% trans "Refresh Pending Notifications" %}
    {% endblock %} From 0fa8af6cd8e833751e3eead2b5ba982f767039f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 00:58:45 +0100 Subject: [PATCH 151/157] translate the notifications too --- InvenTree/InvenTree/urls.py | 1 + InvenTree/templates/account/base.html | 2 +- InvenTree/templates/base.html | 2 +- .../inventree => templates/js/translated}/notification.js | 3 +++ InvenTree/templates/skeleton.html | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) rename InvenTree/{InvenTree/static/script/inventree => templates/js/translated}/notification.js (99%) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 8b0ba630f0..d795b81472 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -134,6 +134,7 @@ translated_javascript_urls = [ url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'), url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'), url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'), + url(r'^notification.js', DynamicJsView.as_view(template_name='js/translated/notification.js'), name='notification.js'), ] backendpatterns = [ diff --git a/InvenTree/templates/account/base.html b/InvenTree/templates/account/base.html index ea3795e87c..6c54faac67 100644 --- a/InvenTree/templates/account/base.html +++ b/InvenTree/templates/account/base.html @@ -91,7 +91,7 @@ - + diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 758917a95f..f916344bf9 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -162,7 +162,6 @@ - @@ -190,6 +189,7 @@ + diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/templates/js/translated/notification.js similarity index 99% rename from InvenTree/InvenTree/static/script/inventree/notification.js rename to InvenTree/templates/js/translated/notification.js index e4b6f884bf..0aca0c2d85 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/templates/js/translated/notification.js @@ -1,3 +1,6 @@ +{% load i18n %} +{% load inventree_extras %} + /* * Add a cached alert message to sesion storage */ diff --git a/InvenTree/templates/skeleton.html b/InvenTree/templates/skeleton.html index ffd634e4f4..9b78a9a329 100644 --- a/InvenTree/templates/skeleton.html +++ b/InvenTree/templates/skeleton.html @@ -77,7 +77,7 @@ - + {% block body_scripts_inventree %} {% endblock %} From a4c9dfdd6f62e6b82f3ef7bd991a335b17195072 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 02:05:18 +0100 Subject: [PATCH 152/157] add read all endpoint --- InvenTree/common/api.py | 22 +++++++++++++++++++ .../InvenTree/notifications/inbox.html | 3 +++ .../notifications/notifications.html | 9 ++++++++ 3 files changed, 34 insertions(+) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index adf99218fa..d725799894 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -14,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt from django.conf.urls import url, include from rest_framework.views import APIView +from rest_framework.response import Response from rest_framework.exceptions import NotAcceptable, NotFound from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters, generics, permissions @@ -314,6 +315,25 @@ class NotificationUnread(NotificationReadEdit): target = False +class NotificationReadAll(generics.RetrieveAPIView): + """ + API endpoint to mark all notifications as read. + """ + + queryset = common.models.NotificationMessage.objects.all() + + permission_classes = [ + UserSettingsPermissions, + ] + + def get(self, request, *args, **kwargs): + try: + self.queryset.filter(user=request.user, read=False).update(read=True) + return Response({'status': 'ok'}) + except Exception as exc: + raise serializers.ValidationError(detail=serializers.as_serializer_error(exc)) + + settings_api_urls = [ # User settings url(r'^user/', include([ @@ -346,6 +366,8 @@ common_api_urls = [ url(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'), url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'), ])), + # Read all + url(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'), # Notification messages list url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'), diff --git a/InvenTree/templates/InvenTree/notifications/inbox.html b/InvenTree/templates/InvenTree/notifications/inbox.html index b50001b107..3a584bc140 100644 --- a/InvenTree/templates/InvenTree/notifications/inbox.html +++ b/InvenTree/templates/InvenTree/notifications/inbox.html @@ -13,6 +13,9 @@
    {% trans "Refresh Pending Notifications" %}
    +
    + {% trans "Mark all as read" %} +
    {% endblock %} {% block content %} diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index ee1f150ae9..89f10c3e3d 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -120,6 +120,15 @@ $("#inbox-refresh").on('click', function() { $("#inbox-table").bootstrapTable('refresh'); }); +$("#mark-all").on('click', function() { + inventreeGet( + '{% url "api-notifications-readall" %}', + { + read: false, + }, + ); + updateNotificationTables(); +}); loadNotificationTable("#history-table", { name: 'history', From c139b905984565d2941aef8f31a370874dddbc87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 02:05:30 +0100 Subject: [PATCH 153/157] move button --- InvenTree/templates/InvenTree/notifications/inbox.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/InvenTree/notifications/inbox.html b/InvenTree/templates/InvenTree/notifications/inbox.html index 3a584bc140..7ec2a27b6a 100644 --- a/InvenTree/templates/InvenTree/notifications/inbox.html +++ b/InvenTree/templates/InvenTree/notifications/inbox.html @@ -10,12 +10,12 @@ {% endblock %} {% block actions %} +
    + {% trans "Mark all as read" %} +
    {% trans "Refresh Pending Notifications" %}
    -
    - {% trans "Mark all as read" %} -
    {% endblock %} {% block content %} From 20b0ccefeaf7aed1ff1adeb094136d2309e73c6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 02:23:11 +0100 Subject: [PATCH 154/157] style fix --- .../templates/js/translated/notification.js | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/InvenTree/templates/js/translated/notification.js b/InvenTree/templates/js/translated/notification.js index 0aca0c2d85..c4f644dbe6 100644 --- a/InvenTree/templates/js/translated/notification.js +++ b/InvenTree/templates/js/translated/notification.js @@ -182,28 +182,28 @@ function updateNotificationReadState(btn, panel_caller=false) { var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`; inventreePut(url, {}, { - method: 'POST', - success: function() { - // update the notification tables if they were declared - if (window.updateNotifications) { - window.updateNotifications(); - } + method: 'POST', + success: function() { + // update the notification tables if they were declared + if (window.updateNotifications) { + window.updateNotifications(); + } - // update current notification count - var count = parseInt($("#notification-counter").html()); - if (btn.attr('target') == 'read') { - count = count - 1; - } else { - count = count + 1; - } - // update notification indicator now - updateNotificationIndicator(count); + // update current notification count + var count = parseInt($('#notification-counter').html()); + if (btn.attr('target') == 'read') { + count = count - 1; + } else { + count = count + 1; + } + // update notification indicator now + updateNotificationIndicator(count); - // remove notification if called from notification panel - if (panel_caller) { - btn.parent().parent().remove() + // remove notification if called from notification panel + if (panel_caller) { + btn.parent().parent().remove(); + } } - } }); }; @@ -259,16 +259,16 @@ function openNotificationPanel() { if (item.target.link) { link_text = `${link_text}`; } - html += link_text + html += link_text; } html += '
    '; html += `${item.age_human}`; html += getReadEditButton(item.pk, item.read, true); - html += "
    "; + html += '
    '; }); // package up - html = `
      ${html}
    ` + html = `
      ${html}
    `; } // set html @@ -297,9 +297,9 @@ function updateNotificationIndicator(count) { notificationUpdateTic = 0; if (count == 0) { - $("#notification-alert").addClass("d-none"); + $('#notification-alert').addClass('d-none'); } else { - $("#notification-alert").removeClass("d-none"); + $('#notification-alert').removeClass('d-none'); } - $("#notification-counter").html(count); + $('#notification-counter').html(count); } From 2861ee0843819fc9703d68e20220a9d373a545cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 02:26:36 +0100 Subject: [PATCH 155/157] another style fix --- InvenTree/templates/js/translated/notification.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/notification.js b/InvenTree/templates/js/translated/notification.js index c4f644dbe6..4da4c2e875 100644 --- a/InvenTree/templates/js/translated/notification.js +++ b/InvenTree/templates/js/translated/notification.js @@ -1,5 +1,13 @@ {% load i18n %} -{% load inventree_extras %} + +/* exported + showAlertOrCache, + showCachedAlerts, + startNotificationWatcher, + stopNotificationWatcher, + openNotificationPanel, + closeNotificationPanel, +*/ /* * Add a cached alert message to sesion storage @@ -123,7 +131,7 @@ function showMessage(message, options={}) { }); } -var notificationWatcher = null; // reference for the notificationWatcher +var notificationWatcher = null; // reference for the notificationWatcher /** * start the regular notification checks **/ From 2bb4e55d4a4418dddd4808e6b0e0605d38637e9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 02:59:08 +0100 Subject: [PATCH 156/157] remove test something complex is happning on gh actions --- InvenTree/InvenTree/tests.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 16e09615aa..669628bdea 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -7,7 +7,6 @@ import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from django.conf import settings -from django.db.utils import IntegrityError from djmoney.money import Money from djmoney.contrib.exchange.models import Rate, convert_money @@ -454,14 +453,6 @@ class TestSettings(TestCase): self.run_reload() self.assertEqual(user_count(), 1) - # enough set - duplicate entry - with self.assertRaises(IntegrityError): - self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username - self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email - self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password - self.run_reload() - self.assertEqual(user_count(), 1) - # make sure to clean up settings.TESTING_ENV = False From eef51600c50f179efbc83e2fab483a29e7d8998e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 18:10:42 +0100 Subject: [PATCH 157/157] do not write to db --- InvenTree/plugin/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index ff03fdd04e..c611f6f8c1 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -32,7 +32,7 @@ class PluginAppConfig(AppConfig): # this is the first startup try: from common.models import InvenTreeSetting - if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP'): + if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False): # make sure all plugins are installed registry.install_plugin_file() except: