mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Notification fix (#3939)
* Fix docstring for NotificationMessage class * Fix for 'refresh' button in notification table * Simplify API for marking notifications as 'read' - Simply update the detail serializer - No requirement for extra API endpoints - Same updates for news feed entry - Hide 'read' news on the home page - Add ability to mark news items as read via table * Bug fix for build.js * Fix for part category template
This commit is contained in:
parent
54e7dd28e5
commit
95645c7b14
@ -19,8 +19,8 @@ import common.models
|
|||||||
import common.serializers
|
import common.serializers
|
||||||
from InvenTree.api import BulkDeleteMixin
|
from InvenTree.api import BulkDeleteMixin
|
||||||
from InvenTree.helpers import inheritors
|
from InvenTree.helpers import inheritors
|
||||||
from InvenTree.mixins import (CreateAPI, ListAPI, RetrieveAPI,
|
from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI,
|
||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
RetrieveUpdateDestroyAPI)
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting
|
||||||
from plugin.serializers import NotificationUserSettingSerializer
|
from plugin.serializers import NotificationUserSettingSerializer
|
||||||
|
|
||||||
@ -319,36 +319,6 @@ class NotificationDetail(NotificationMessageMixin, RetrieveUpdateDestroyAPI):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class NotificationReadEdit(NotificationMessageMixin, CreateAPI):
|
|
||||||
"""General API endpoint to manipulate read state of a notification."""
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
"""Add instance to context so it can be accessed in the serializer."""
|
|
||||||
context = super().get_serializer_context()
|
|
||||||
if self.request:
|
|
||||||
context['instance'] = self.get_object()
|
|
||||||
return context
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
"""Set the `read` status to the target value."""
|
|
||||||
message = self.get_object()
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationReadAll(NotificationMessageMixin, RetrieveAPI):
|
class NotificationReadAll(NotificationMessageMixin, RetrieveAPI):
|
||||||
"""API endpoint to mark all notifications as read."""
|
"""API endpoint to mark all notifications as read."""
|
||||||
|
|
||||||
@ -390,11 +360,6 @@ class NewsFeedEntryDetail(NewsFeedMixin, RetrieveUpdateDestroyAPI):
|
|||||||
"""Detail view for an individual news feed object."""
|
"""Detail view for an individual news feed object."""
|
||||||
|
|
||||||
|
|
||||||
class NewsFeedEntryRead(NewsFeedMixin, NotificationReadEdit):
|
|
||||||
"""API endpoint to mark a news item as read."""
|
|
||||||
target = True
|
|
||||||
|
|
||||||
|
|
||||||
settings_api_urls = [
|
settings_api_urls = [
|
||||||
# User settings
|
# User settings
|
||||||
re_path(r'^user/', include([
|
re_path(r'^user/', include([
|
||||||
@ -432,8 +397,6 @@ common_api_urls = [
|
|||||||
re_path(r'^notifications/', include([
|
re_path(r'^notifications/', include([
|
||||||
# Individual purchase order detail URLs
|
# Individual purchase order detail URLs
|
||||||
re_path(r'^(?P<pk>\d+)/', include([
|
re_path(r'^(?P<pk>\d+)/', include([
|
||||||
re_path(r'^read/', NotificationRead.as_view(), name='api-notifications-read'),
|
|
||||||
re_path(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'),
|
|
||||||
re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
|
re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
|
||||||
])),
|
])),
|
||||||
# Read all
|
# Read all
|
||||||
@ -446,7 +409,6 @@ common_api_urls = [
|
|||||||
# News
|
# News
|
||||||
re_path(r'^news/', include([
|
re_path(r'^news/', include([
|
||||||
re_path(r'^(?P<pk>\d+)/', include([
|
re_path(r'^(?P<pk>\d+)/', include([
|
||||||
re_path(r'^read/', NewsFeedEntryRead.as_view(), name='api-news-read'),
|
|
||||||
re_path(r'.*$', NewsFeedEntryDetail.as_view(), name='api-news-detail'),
|
re_path(r'.*$', NewsFeedEntryDetail.as_view(), name='api-news-detail'),
|
||||||
])),
|
])),
|
||||||
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
|
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
|
||||||
|
@ -2199,14 +2199,13 @@ class NotificationEntry(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class NotificationMessage(models.Model):
|
class NotificationMessage(models.Model):
|
||||||
"""A NotificationEntry records the last time a particular notifaction was sent out.
|
"""A NotificationMessage is a message sent to a particular user, notifying them of some *important information*
|
||||||
|
|
||||||
It is recorded to ensure that notifications are not sent out "too often" to users.
|
Notification messages can be generated by a variety of sources.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
- key: A text entry describing the notification e.g. 'part.notify_low_stock'
|
target_object: The 'target' of the notification message
|
||||||
- uid: An (optional) numerical ID for a particular instance
|
source_object: The 'source' of the notification message
|
||||||
- date: The last time this notification was sent
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# generic link to target
|
# generic link to target
|
||||||
|
@ -158,7 +158,7 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
age_human = serializers.CharField(read_only=True)
|
age_human = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
read = serializers.BooleanField(read_only=True)
|
read = serializers.BooleanField()
|
||||||
|
|
||||||
def get_target(self, obj):
|
def get_target(self, obj):
|
||||||
"""Function to resolve generic object reference to target."""
|
"""Function to resolve generic object reference to target."""
|
||||||
@ -203,20 +203,10 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NotificationReadSerializer(NotificationMessageSerializer):
|
|
||||||
"""Serializer for reading a notification."""
|
|
||||||
|
|
||||||
def is_valid(self, raise_exception=False):
|
|
||||||
"""Ensure instance data is available for view and let validation pass."""
|
|
||||||
self.instance = self.context['instance'] # set instance that should be returned
|
|
||||||
self._validated_data = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class NewsFeedEntrySerializer(InvenTreeModelSerializer):
|
class NewsFeedEntrySerializer(InvenTreeModelSerializer):
|
||||||
"""Serializer for the NewsFeedEntry model."""
|
"""Serializer for the NewsFeedEntry model."""
|
||||||
|
|
||||||
read = serializers.BooleanField(read_only=True)
|
read = serializers.BooleanField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for NewsFeedEntrySerializer."""
|
"""Meta options for NewsFeedEntrySerializer."""
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
{% if user.is_staff and roles.part_category.change %}
|
{% if category and user.is_staff and roles.part_category.change %}
|
||||||
{% url 'admin:part_partcategory_change' category.pk as url %}
|
{% url 'admin:part_partcategory_change' category.pk as url %}
|
||||||
{% include "admin_button.html" with url=url %}
|
{% include "admin_button.html" with url=url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -2679,7 +2679,7 @@ function loadBuildTable(table, options) {
|
|||||||
treeColumn: 1,
|
treeColumn: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
table.treegrid('expandAll');
|
$(table).treegrid('expandAll');
|
||||||
} else if (display_mode == 'calendar') {
|
} else if (display_mode == 'calendar') {
|
||||||
|
|
||||||
if (!loaded_calendar) {
|
if (!loaded_calendar) {
|
||||||
|
@ -17,6 +17,7 @@ function loadNewsFeedTable(table, options={}, enableDelete=false) {
|
|||||||
groupBy: false,
|
groupBy: false,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
ordering: 'published',
|
ordering: 'published',
|
||||||
|
read: false,
|
||||||
},
|
},
|
||||||
paginationVAlign: 'bottom',
|
paginationVAlign: 'bottom',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
@ -49,10 +50,31 @@ function loadNewsFeedTable(table, options={}, enableDelete=false) {
|
|||||||
field: 'published',
|
field: 'published',
|
||||||
title: '{% trans "Published" %}',
|
title: '{% trans "Published" %}',
|
||||||
sortable: 'true',
|
sortable: 'true',
|
||||||
formatter: function(value) {
|
formatter: function(value, row) {
|
||||||
return renderDate(value);
|
var html = renderDate(value);
|
||||||
|
var buttons = getReadEditButton(row.pk, row.read);
|
||||||
|
html += `<div class='btn-group float-right' role='group'>${buttons}</div>`;
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(table).on('click', '.notification-read', function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
var url = `/api/news/${pk}/`;
|
||||||
|
|
||||||
|
inventreePut(url,
|
||||||
|
{
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
success: function() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ function loadNotificationTable(table, options={}, enableDelete=false) {
|
|||||||
var params = options.params || {};
|
var params = options.params || {};
|
||||||
var read = typeof(params.read) === 'undefined' ? true : params.read;
|
var read = typeof(params.read) === 'undefined' ? true : params.read;
|
||||||
|
|
||||||
setupFilterList(`notifications-${options.name}`, table);
|
setupFilterList(`notifications-${options.name}`, $(table));
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
@ -157,10 +157,20 @@ function notificationCheck(force = false) {
|
|||||||
* - panel_caller: this button was clicked in the notification panel
|
* - panel_caller: this button was clicked in the notification panel
|
||||||
**/
|
**/
|
||||||
function updateNotificationReadState(btn, panel_caller=false) {
|
function updateNotificationReadState(btn, panel_caller=false) {
|
||||||
var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`;
|
|
||||||
|
|
||||||
inventreePut(url, {}, {
|
// Determine 'read' status of the notification
|
||||||
method: 'POST',
|
var status = btn.attr('target') == 'read';
|
||||||
|
var pk = btn.attr('pk');
|
||||||
|
|
||||||
|
var url = `/api/notifications/${pk}/`;
|
||||||
|
|
||||||
|
inventreePut(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
read: status,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
success: function() {
|
success: function() {
|
||||||
// update the notification tables if they were declared
|
// update the notification tables if they were declared
|
||||||
if (window.updateNotifications) {
|
if (window.updateNotifications) {
|
||||||
@ -169,7 +179,8 @@ function updateNotificationReadState(btn, panel_caller=false) {
|
|||||||
|
|
||||||
// update current notification count
|
// update current notification count
|
||||||
var count = parseInt($('#notification-counter').html());
|
var count = parseInt($('#notification-counter').html());
|
||||||
if (btn.attr('target') == 'read') {
|
|
||||||
|
if (status) {
|
||||||
count = count - 1;
|
count = count - 1;
|
||||||
} else {
|
} else {
|
||||||
count = count + 1;
|
count = count + 1;
|
||||||
@ -188,7 +199,8 @@ function updateNotificationReadState(btn, panel_caller=false) {
|
|||||||
btn.parent().parent().remove();
|
btn.parent().parent().remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user