2
0
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:
Oliver 2022-11-17 08:26:19 +11:00 committed by GitHub
parent 54e7dd28e5
commit 95645c7b14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 72 additions and 87 deletions

View File

@ -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'),

View File

@ -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

View File

@ -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."""

View File

@ -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 %}

View File

@ -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) {

View File

@ -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');
}
}
);
});
} }

View File

@ -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,38 +157,50 @@ 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';
success: function() { var pk = btn.attr('pk');
// update the notification tables if they were declared
if (window.updateNotifications) {
window.updateNotifications();
}
// update current notification count var url = `/api/notifications/${pk}/`;
var count = parseInt($('#notification-counter').html());
if (btn.attr('target') == 'read') {
count = count - 1;
} else {
count = count + 1;
}
// Prevent negative notification count inventreePut(
if (count < 0) { url,
count = 0; {
} read: status,
},
{
method: 'PATCH',
success: function() {
// update the notification tables if they were declared
if (window.updateNotifications) {
window.updateNotifications();
}
// update notification indicator now // update current notification count
updateNotificationIndicator(count); var count = parseInt($('#notification-counter').html());
// remove notification if called from notification panel if (status) {
if (panel_caller) { count = count - 1;
btn.parent().parent().remove(); } else {
count = count + 1;
}
// Prevent negative notification count
if (count < 0) {
count = 0;
}
// update notification indicator now
updateNotificationIndicator(count);
// remove notification if called from notification panel
if (panel_caller) {
btn.parent().parent().remove();
}
} }
} }
}); );
}; };
/** /**