Fix update notification deduplication and release link (#12424)

This commit is contained in:
Leonid Zaliubovskyi
2026-07-20 14:33:25 +10:00
committed by GitHub
parent fcbe4302b9
commit 3ddf792a59
7 changed files with 92 additions and 11 deletions
+3
View File
@@ -856,6 +856,7 @@ def check_for_updates():
data = json.loads(response.text) data = json.loads(response.text)
tag = data.get('tag_name', None) tag = data.get('tag_name', None)
release_url = data.get('html_url')
if not tag: if not tag:
raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover
@@ -885,11 +886,13 @@ def check_for_updates():
trigger_notification( trigger_notification(
None, None,
'update_available', 'update_available',
notification_uid=0,
targets=get_user_model().objects.filter(is_superuser=True), targets=get_user_model().objects.filter(is_superuser=True),
delivery_methods={InvenTreeUINotifications}, delivery_methods={InvenTreeUINotifications},
context={ context={
'name': _('Update Available'), 'name': _('Update Available'),
'message': _('An update for InvenTree is available'), 'message': _('An update for InvenTree is available'),
'link': release_url,
}, },
) )
+50 -10
View File
@@ -2,6 +2,7 @@
import os import os
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -11,6 +12,7 @@ from django.db.utils import NotSupportedError
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
import requests_mock
from django_q.models import OrmQ, Schedule, Task from django_q.models import OrmQ, Schedule, Task
from error_report.models import Error from error_report.models import Error
@@ -168,19 +170,57 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
errors = Error.objects.filter(when__lte=threshold) errors = Error.objects.filter(when__lte=threshold)
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
def test_task_check_for_updates(self): @requests_mock.Mocker()
"""Test the task check_for_updates.""" def test_task_check_for_updates(self, requests_mocker):
# Check that setting should be empty """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', '') InvenTreeSetting.set_setting('_INVENTREE_LATEST_VERSION', '')
self.assertEqual(InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION'), '')
# Get new version with (
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates) 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 self.assertEqual(
response = InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION') InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION'), '99.99.99'
self.assertNotEqual(response, '') )
self.assertTrue(bool(response)) 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): def test_task_check_for_migrations(self):
"""Test the task check_for_migrations.""" """Test the task check_for_migrations."""
@@ -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,
),
)
]
+7
View File
@@ -1723,6 +1723,13 @@ class NotificationMessage(models.Model):
message = models.CharField(max_length=250, blank=True, null=True) 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) creation = models.DateTimeField(auto_now_add=True)
read = models.BooleanField(default=False) read = models.BooleanField(default=False)
@@ -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 obj: The object (model instance) that is triggering the notification
category: The category (label) for the notification category: The category (label) for the notification
obj_ref: The reference to the object that should be used 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 kwargs: Additional arguments to pass to the notification method
""" """
# Check if data is importing currently # 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', {}) context = kwargs.get('context', {})
delivery_methods = kwargs.get('delivery_methods') delivery_methods = kwargs.get('delivery_methods')
check_recent = kwargs.get('check_recent', True) check_recent = kwargs.get('check_recent', True)
notification_uid = kwargs.get('notification_uid')
# Resolve object reference # Resolve object reference
refs = [obj_ref, 'pk', 'id', 'uid'] refs = [obj_ref, 'pk', 'id', 'uid']
@@ -122,6 +124,8 @@ def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', **
raise KeyError( raise KeyError(
f"Could not resolve an object reference for '{obj!s}' with {','.join(set(refs))}" 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... # Check if we have notified recently...
delta = timedelta(days=1) delta = timedelta(days=1)
+3 -1
View File
@@ -318,7 +318,9 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
"""Function to resolve generic object reference to target.""" """Function to resolve generic object reference to target."""
target = get_objectreference(obj, 'target_content_type', 'target_object_id') 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 # Check if object has an absolute_url function
if hasattr(obj.target_object, 'get_absolute_url'): if hasattr(obj.target_object, 'get_absolute_url'):
target['link'] = obj.target_object.get_absolute_url() target['link'] = obj.target_object.get_absolute_url()
@@ -52,6 +52,7 @@ class InvenTreeUINotifications(NotificationMixin, InvenTreePlugin):
category=category, category=category,
name=ctx.get('name'), name=ctx.get('name'),
message=ctx.get('message'), message=ctx.get('message'),
link=ctx.get('link'),
) )
) )