2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-02 19:50:59 +00:00

Add news reader (#3445)

* add model for feed entries

* add task to update feed entries

* Add API routes

* Fix name in model

* rename model

* fix read endpoint

* reduce duplication in NewsFeed API endpoints

* reduce duplicated code

* add ui elements to index

* add missing migrations

* add ressource route

* add new model to admin

* reorder fields

* format timestamp

* make title linked

* reduce migrations to 1

* fix merge

* fix js style

* add model to ruleset
This commit is contained in:
Matthias Mair
2022-11-10 02:20:06 +01:00
committed by GitHub
parent f6cfc12343
commit fb77158496
15 changed files with 291 additions and 33 deletions

View File

@ -55,9 +55,16 @@ class NotificationMessageAdmin(admin.ModelAdmin):
search_fields = ('name', 'category', 'message', )
class NewsFeedEntryAdmin(admin.ModelAdmin):
"""Admin settings for NewsFeedEntry."""
list_display = ('title', 'author', 'published', 'summary', )
admin.site.register(common.models.InvenTreeSetting, SettingsAdmin)
admin.site.register(common.models.InvenTreeUserSetting, UserSettingsAdmin)
admin.site.register(common.models.WebhookEndpoint, WebhookAdmin)
admin.site.register(common.models.WebhookMessage, ImportExportModelAdmin)
admin.site.register(common.models.NotificationEntry, NotificationEntryAdmin)
admin.site.register(common.models.NotificationMessage, NotificationMessageAdmin)
admin.site.register(common.models.NewsFeedEntry, NewsFeedEntryAdmin)

View File

@ -11,6 +11,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_q.tasks import async_task
from rest_framework import filters, permissions, serializers
from rest_framework.exceptions import NotAcceptable, NotFound
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.views import APIView
@ -255,21 +256,20 @@ class NotificationUserSettingsDetail(RetrieveUpdateAPI):
queryset = NotificationUserSetting.objects.all()
serializer_class = NotificationUserSettingSerializer
permission_classes = [
UserSettingsPermissions,
]
permission_classes = [UserSettingsPermissions, ]
class NotificationList(BulkDeleteMixin, ListAPI):
"""List view for all notifications of the current user."""
class NotificationMessageMixin:
"""Generic mixin for NotificationMessage."""
queryset = common.models.NotificationMessage.objects.all()
serializer_class = common.serializers.NotificationMessageSerializer
permission_classes = [UserSettingsPermissions, ]
permission_classes = [
permissions.IsAuthenticated,
]
class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
"""List view for all notifications of the current user."""
permission_classes = [permissions.IsAuthenticated, ]
filter_backends = [
DjangoFilterBackend,
@ -312,29 +312,16 @@ class NotificationList(BulkDeleteMixin, ListAPI):
return queryset
class NotificationDetail(RetrieveUpdateDestroyAPI):
class NotificationDetail(NotificationMessageMixin, RetrieveUpdateDestroyAPI):
"""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,
]
class NotificationReadEdit(CreateAPI):
class NotificationReadEdit(NotificationMessageMixin, CreateAPI):
"""General API endpoint to manipulate read state of a notification."""
queryset = common.models.NotificationMessage.objects.all()
serializer_class = common.serializers.NotificationReadSerializer
permission_classes = [
UserSettingsPermissions,
]
def get_serializer_context(self):
"""Add instance to context so it can be accessed in the serializer."""
context = super().get_serializer_context()
@ -362,15 +349,9 @@ class NotificationUnread(NotificationReadEdit):
target = False
class NotificationReadAll(RetrieveAPI):
class NotificationReadAll(NotificationMessageMixin, RetrieveAPI):
"""API endpoint to mark all notifications as read."""
queryset = common.models.NotificationMessage.objects.all()
permission_classes = [
UserSettingsPermissions,
]
def get(self, request, *args, **kwargs):
"""Set all messages for the current user as read."""
try:
@ -380,6 +361,40 @@ class NotificationReadAll(RetrieveAPI):
raise serializers.ValidationError(detail=serializers.as_serializer_error(exc))
class NewsFeedMixin:
"""Generic mixin for NewsFeedEntry."""
queryset = common.models.NewsFeedEntry.objects.all()
serializer_class = common.serializers.NewsFeedEntrySerializer
permission_classes = [IsAdminUser, ]
class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI):
"""List view for all news items."""
filter_backends = [
DjangoFilterBackend,
filters.OrderingFilter,
]
ordering_fields = [
'published',
'author',
'read',
]
filterset_fields = [
'read',
]
class NewsFeedEntryDetail(NewsFeedMixin, RetrieveUpdateDestroyAPI):
"""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 = [
# User settings
re_path(r'^user/', include([
@ -428,4 +443,13 @@ common_api_urls = [
re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
])),
# News
re_path(r'^news/', 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'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
])),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.14 on 2022-07-31 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0014_notificationmessage'),
]
operations = [
migrations.CreateModel(
name='NewsFeedEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('feed_id', models.CharField(max_length=250, unique=True, verbose_name='Id')),
('title', models.CharField(max_length=250, verbose_name='Title')),
('link', models.URLField(max_length=250, verbose_name='Link')),
('published', models.DateTimeField(max_length=250, verbose_name='Published')),
('author', models.CharField(max_length=250, verbose_name='Author')),
('summary', models.CharField(max_length=250, verbose_name='Summary')),
('read', models.BooleanField(default=False, help_text='Was this news item read?', verbose_name='Read')),
],
),
]

View File

@ -1560,6 +1560,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'validator': bool,
},
'HOMEPAGE_NEWS': {
'name': _('Show News'),
'description': _('Show news on the homepage'),
'default': False,
'validator': bool,
},
"LABEL_INLINE": {
'name': _('Inline label display'),
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
@ -2285,3 +2292,54 @@ class NotificationMessage(models.Model):
def age_human(self):
"""Humanized age."""
return naturaltime(self.creation)
class NewsFeedEntry(models.Model):
"""A NewsFeedEntry represents an entry on the RSS/Atom feed that is generated for InvenTree news.
Attributes:
- feed_id: Unique id for the news item
- title: Title for the news item
- link: Link to the news item
- published: Date of publishing of the news item
- author: Author of news item
- summary: Summary of the news items content
- read: Was this iteam already by a superuser?
"""
feed_id = models.CharField(
verbose_name=_('Id'),
unique=True,
max_length=250,
)
title = models.CharField(
verbose_name=_('Title'),
max_length=250,
)
link = models.URLField(
verbose_name=_('Link'),
max_length=250,
)
published = models.DateTimeField(
verbose_name=_('Published'),
max_length=250,
)
author = models.CharField(
verbose_name=_('Author'),
max_length=250,
)
summary = models.CharField(
verbose_name=_('Summary'),
max_length=250,
)
read = models.BooleanField(
verbose_name=_('Read'),
help_text=_('Was this news item read?'),
default=False
)

View File

@ -5,7 +5,7 @@ from django.urls import reverse
from rest_framework import serializers
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
NotificationMessage)
NewsFeedEntry, NotificationMessage)
from InvenTree.helpers import construct_absolute_url, get_objectreference
from InvenTree.serializers import InvenTreeModelSerializer
@ -211,3 +211,24 @@ class NotificationReadSerializer(NotificationMessageSerializer):
self.instance = self.context['instance'] # set instance that should be returned
self._validated_data = True
return True
class NewsFeedEntrySerializer(InvenTreeModelSerializer):
"""Serializer for the NewsFeedEntry model."""
read = serializers.BooleanField(read_only=True)
class Meta:
"""Meta options for NewsFeedEntrySerializer."""
model = NewsFeedEntry
fields = [
'pk',
'feed_id',
'title',
'link',
'published',
'author',
'summary',
'read',
]

View File

@ -3,8 +3,11 @@
import logging
from datetime import datetime, timedelta
from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
import feedparser
from InvenTree.tasks import ScheduledTask, scheduled_task
logger = logging.getLogger('inventree')
@ -26,3 +29,41 @@ def delete_old_notifications():
# Delete notification records before the specified date
NotificationEntry.objects.filter(updated__lte=before).delete()
@scheduled_task(ScheduledTask.DAILY)
def update_news_feed():
"""Update the newsfeed."""
try:
from common.models import NewsFeedEntry
except AppRegistryNotReady: # pragma: no cover
logger.info("Could not perform 'update_news_feed' - App registry not ready")
return
# Fetch and parse feed
try:
d = feedparser.parse(settings.INVENTREE_NEWS_URL)
except Exception as entry: # pragma: no cover
logger.warning("update_news_feed: Error parsing the newsfeed", entry)
return
# Get a reference list
id_list = [a.feed_id for a in NewsFeedEntry.objects.all()]
# Iterate over entries
for entry in d.entries:
# Check if id already exsists
if entry.id in id_list:
continue
# Create entry
NewsFeedEntry.objects.create(
feed_id=entry.id,
title=entry.title,
link=entry.link,
published=entry.published,
author=entry.author,
summary=entry.summary,
)
logger.info('update_news_feed: Sync done')