Fix update notification deduplication and release link (#12424)

This commit is contained in:
Leonid Zaliubovskyi 2026-07-20 06:33:25 +02:00 committed by GitHub
parent fcbe4302b9
commit 3ddf792a59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 92 additions and 11 deletions

View File

@ -856,6 +856,7 @@ def check_for_updates():
data = json.loads(response.text)
tag = data.get('tag_name', None)
release_url = data.get('html_url')
if not tag:
raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover
@ -885,11 +886,13 @@ def check_for_updates():
trigger_notification(
None,
'update_available',
notification_uid=0,
targets=get_user_model().objects.filter(is_superuser=True),
delivery_methods={InvenTreeUINotifications},
context={
'name': _('Update Available'),
'message': _('An update for InvenTree is available'),
'link': release_url,
},
)

View File

@ -2,6 +2,7 @@
import os
from datetime import timedelta
from unittest.mock import patch
from django.conf import settings
from django.contrib.auth.models import User
@ -11,6 +12,7 @@ from django.db.utils import NotSupportedError
from django.test import TestCase
from django.utils import timezone
import requests_mock
from django_q.models import OrmQ, Schedule, Task
from error_report.models import Error
@ -168,19 +170,57 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
errors = Error.objects.filter(when__lte=threshold)
self.assertEqual(len(errors), 0)
def test_task_check_for_updates(self):
"""Test the task check_for_updates."""
# Check that setting should be empty
@requests_mock.Mocker()
def test_task_check_for_updates(self, requests_mocker):
"""Test update notification creation and deduplication."""
from common.models import NotificationEntry, NotificationMessage
from common.serializers import NotificationMessageSerializer
self.ensurePluginsLoaded(force=True)
user = User.objects.create_superuser(
username='update_admin',
email='update@example.com',
password='test-password',
)
release_url = 'https://github.com/InvenTree/InvenTree/releases/tag/99.99.99'
requests_mocker.get(
'https://api.github.com/repos/inventree/inventree/releases/latest',
json={'tag_name': '99.99.99', 'html_url': release_url},
)
InvenTreeSetting.set_setting('_INVENTREE_LATEST_VERSION', '')
self.assertEqual(InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION'), '')
# Get new version
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates)
with (
patch(
'InvenTree.tasks.isInvenTreeUpToDate', return_value=False
) as mock_up_to_date,
patch('InvenTree.tasks.check_daily_holdoff', return_value=True),
):
InvenTree.tasks.check_for_updates()
# Check that setting is not empty
response = InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION')
self.assertNotEqual(response, '')
self.assertTrue(bool(response))
self.assertEqual(
InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION'), '99.99.99'
)
self.assertEqual(NotificationMessage.objects.count(), 1)
self.assertEqual(NotificationEntry.objects.count(), 1)
message = NotificationMessage.objects.get(user=user)
entry = NotificationEntry.objects.get()
self.assertEqual(message.link, release_url)
self.assertEqual(entry.uid, 0)
serialized = NotificationMessageSerializer(message).data
self.assertEqual(serialized['target']['link'], release_url)
InvenTree.tasks.check_for_updates()
self.assertEqual(NotificationMessage.objects.count(), 1)
self.assertEqual(NotificationEntry.objects.count(), 1)
mock_up_to_date.assert_called()
def test_task_check_for_migrations(self):
"""Test the task check_for_migrations."""

View File

@ -0,0 +1,24 @@
"""Add link field to notification messages."""
# Generated by Django 5.2.15 on 2026-07-20 01:09
from django.db import migrations, models
class Migration(migrations.Migration):
"""Add the optional notification link field."""
dependencies = [('common', '0047_parametertemplate_unique')]
operations = [
migrations.AddField(
model_name='notificationmessage',
name='link',
field=models.URLField(
blank=True,
help_text='Optional explicit URL associated with this notification',
max_length=500,
null=True,
),
)
]

View File

@ -1723,6 +1723,13 @@ class NotificationMessage(models.Model):
message = models.CharField(max_length=250, blank=True, null=True)
link = models.URLField(
max_length=500,
blank=True,
null=True,
help_text=_('Optional explicit URL associated with this notification'),
)
creation = models.DateTimeField(auto_now_add=True)
read = models.BooleanField(default=False)

View File

@ -91,6 +91,7 @@ def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', **
obj: The object (model instance) that is triggering the notification
category: The category (label) for the notification
obj_ref: The reference to the object that should be used for the notification
notification_uid: Explicit deduplication identifier for notifications without a model instance
kwargs: Additional arguments to pass to the notification method
"""
# Check if data is importing currently
@ -105,6 +106,7 @@ def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', **
context = kwargs.get('context', {})
delivery_methods = kwargs.get('delivery_methods')
check_recent = kwargs.get('check_recent', True)
notification_uid = kwargs.get('notification_uid')
# Resolve object reference
refs = [obj_ref, 'pk', 'id', 'uid']
@ -122,6 +124,8 @@ def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', **
raise KeyError(
f"Could not resolve an object reference for '{obj!s}' with {','.join(set(refs))}"
)
elif notification_uid is not None:
obj_ref_value = notification_uid
# Check if we have notified recently...
delta = timedelta(days=1)

View File

@ -318,7 +318,9 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
"""Function to resolve generic object reference to target."""
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
if target and 'link' not in target:
if target and obj.link:
target['link'] = obj.link
elif target and 'link' not in target:
# Check if object has an absolute_url function
if hasattr(obj.target_object, 'get_absolute_url'):
target['link'] = obj.target_object.get_absolute_url()

View File

@ -52,6 +52,7 @@ class InvenTreeUINotifications(NotificationMixin, InvenTreePlugin):
category=category,
name=ctx.get('name'),
message=ctx.get('message'),
link=ctx.get('link'),
)
)