add a notification message model

part of #2282
This commit is contained in:
Matthias 2021-11-27 16:02:23 +01:00
parent 6b53fd2bd4
commit dd44eb389f
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
6 changed files with 296 additions and 79 deletions

View File

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

View File

@ -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<pk>\d+)/', NotificationDetail.as_view(), name='api-notifications-detail'),
url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
])),
]

View File

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

View File

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

View File

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

View File

@ -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 = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
function loadNotificationTable(table, options={}) {
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
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 = "<button title='{% trans "Mark as read" %}' class='notification-read btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-check icon-green'></span></button>";
var html = "<div class='btn-group float-right' role='group'>" + bRead + "</div>";
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 = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
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() {