mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 12:30:15 +02:00
Notifications refresh v2 and session caching
This commit is contained in:
parent
aeeb35bc60
commit
4394d25961
@ -6,8 +6,9 @@ from django.contrib.auth import login, authenticate
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import signing
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
|
@ -1,22 +1,9 @@
|
||||
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_NOTIFICATIONS = 50
|
||||
|
||||
|
||||
def notify(user, title, message=None, level='info'):
|
||||
def notify(
|
||||
user: object, title: str, message: str = None, level: str = 'info'
|
||||
) -> None:
|
||||
"""Sends a new notification to user. Convenience function to manager pendant."""
|
||||
from .models import Notification
|
||||
if Notification.objects.filter(user=user).count() > MAX_NOTIFICATIONS:
|
||||
for n in Notification.objects.filter(user=user)[MAX_NOTIFICATIONS-1:]:
|
||||
n.delete()
|
||||
notif = Notification()
|
||||
notif.user = user
|
||||
notif.title = title
|
||||
if not message:
|
||||
message = title
|
||||
notif.message = message
|
||||
notif.level = level
|
||||
notif.save()
|
||||
logger.info("Created notification %s" % notif)
|
||||
Notification.objects.notify_user(user, title, message, level)
|
||||
|
@ -1,11 +0,0 @@
|
||||
from .models import Notification
|
||||
from django.core.cache import cache
|
||||
|
||||
def user_notification_count(request):
|
||||
user_id = request.user.id
|
||||
notification_count = cache.get("u-note:{}".format(user_id), -1)
|
||||
if notification_count<0:
|
||||
notification_count = Notification.objects.filter(user__id=user_id).filter(viewed=False).count()
|
||||
cache.set("u-note:{}".format(user_id),notification_count,5)
|
||||
|
||||
return {'notifications': notification_count}
|
101
allianceauth/notifications/managers.py
Normal file
101
allianceauth/notifications/managers.py
Normal file
@ -0,0 +1,101 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationQuerySet(models.QuerySet):
|
||||
"""Custom QuerySet for Notification model"""
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
# overriden update to ensure cache is invaidated on very call
|
||||
super().update(*args, **kwargs)
|
||||
user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
|
||||
for user_pk in user_pks:
|
||||
NotificationManager.invalidate_user_notification_cache(user_pk)
|
||||
|
||||
|
||||
class NotificationManager(models.Manager):
|
||||
|
||||
USER_NOTIFICATION_COUNT_PREFIX = 'USER_NOTIFICATION_COUNT'
|
||||
USER_NOTIFICATION_COUNT_CACHE_DURATION = 86_400
|
||||
|
||||
def get_queryset(self):
|
||||
return NotificationQuerySet(self.model, using=self._db)
|
||||
|
||||
def notify_user(
|
||||
self, user: object, title: str, message: str = None, level: str = 'info'
|
||||
) -> object:
|
||||
"""Sends a new notification to user. Returns newly created notification object.
|
||||
"""
|
||||
max_notifications = self._max_notifications_per_user()
|
||||
if self.filter(user=user).count() >= max_notifications:
|
||||
to_be_deleted_qs = self.filter(user=user).order_by(
|
||||
"-timestamp"
|
||||
)[max_notifications - 1:]
|
||||
for notification in to_be_deleted_qs:
|
||||
notification.delete()
|
||||
|
||||
if not message:
|
||||
message = title
|
||||
|
||||
obj = self.create(user=user, title=title, message=message, level=level)
|
||||
logger.info("Created notification %s", obj)
|
||||
return obj
|
||||
|
||||
def _max_notifications_per_user(self):
|
||||
"""return the maximum number of notifications allowed per user"""
|
||||
max_notifications = getattr(settings, 'NOTIFICATIONS_MAX_PER_USER', None)
|
||||
if (
|
||||
max_notifications is None
|
||||
or not isinstance(max_notifications, int)
|
||||
or max_notifications < 0
|
||||
):
|
||||
logger.warning(
|
||||
'NOTIFICATIONS_MAX_PER_USER setting is invalid. Using default.'
|
||||
)
|
||||
max_notifications = self.model.NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
|
||||
return max_notifications
|
||||
|
||||
def user_unread_count(self, user_pk: int) -> int:
|
||||
"""returns the cached unread count for a user given by user PK
|
||||
|
||||
Will return -1 if user can not be found
|
||||
"""
|
||||
cache_key = self._user_notification_cache_key(user_pk)
|
||||
unread_count = cache.get(key=cache_key)
|
||||
if not unread_count:
|
||||
try:
|
||||
user = User.objects.get(pk=user_pk)
|
||||
except User.DoesNotExist:
|
||||
unread_count = -1
|
||||
else:
|
||||
logger.debug(
|
||||
'Updating notification cache for user with pk %s', user_pk
|
||||
)
|
||||
unread_count = user.notification_set.filter(viewed=False).count()
|
||||
cache.set(
|
||||
key=cache_key,
|
||||
value=unread_count,
|
||||
timeout=self.USER_NOTIFICATION_COUNT_CACHE_DURATION
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
'Returning notification count from cache for user with pk %s', user_pk
|
||||
)
|
||||
|
||||
return unread_count
|
||||
|
||||
@classmethod
|
||||
def invalidate_user_notification_cache(cls, user_pk: int) -> None:
|
||||
cache.delete(key=cls._user_notification_cache_key(user_pk))
|
||||
logger.debug('Invalided notification cache for user with pk %s', user_pk)
|
||||
|
||||
@classmethod
|
||||
def _user_notification_cache_key(cls, user_pk: int) -> str:
|
||||
return f'{cls.USER_NOTIFICATION_COUNT_PREFIX}_{user_pk}'
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-07 21:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0003_make_strings_more_stringy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='notification',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='viewed',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
]
|
@ -1,11 +1,19 @@
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
import logging
|
||||
|
||||
from .managers import NotificationManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
"""Notification to a user within Auth"""
|
||||
|
||||
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
|
||||
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
|
||||
|
||||
LEVEL_CHOICES = (
|
||||
('danger', 'CRITICAL'),
|
||||
('danger', 'ERROR'),
|
||||
@ -18,19 +26,41 @@ class Notification(models.Model):
|
||||
level = models.CharField(choices=LEVEL_CHOICES, max_length=10)
|
||||
title = models.CharField(max_length=254)
|
||||
message = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
viewed = models.BooleanField(default=False)
|
||||
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
viewed = models.BooleanField(default=False, db_index=True)
|
||||
|
||||
def view(self):
|
||||
objects = NotificationManager()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s: %s" % (self.user, self.title)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# overriden save to ensure cache is invaidated on very call
|
||||
super().save(*args, **kwargs)
|
||||
Notification.objects.invalidate_user_notification_cache(self.user.pk)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# overriden delete to ensure cache is invaidated on very call
|
||||
super().delete(*args, **kwargs)
|
||||
Notification.objects.invalidate_user_notification_cache(self.user.pk)
|
||||
|
||||
def mark_viewed(self) -> None:
|
||||
"""mark notification as viewed"""
|
||||
logger.info("Marking notification as viewed: %s" % self)
|
||||
self.viewed = True
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return "%s: %s" % (self.user, self.title)
|
||||
def set_level(self, level_name: str) -> None:
|
||||
"""set notification level according to level name, e.g. 'CRITICAL'
|
||||
|
||||
def set_level(self, level):
|
||||
self.level = [item[0] for item in self.LEVEL_CHOICES if item[1] == level][0]
|
||||
raised exception on invalid level names
|
||||
"""
|
||||
try:
|
||||
new_level = [
|
||||
item[0] for item in self.LEVEL_CHOICES if item[1] == level_name
|
||||
][0]
|
||||
except IndexError:
|
||||
raise ValueError('Invalid level name: %s' % level_name)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-timestamp']
|
||||
self.level = new_level
|
||||
self.save()
|
||||
|
0
allianceauth/notifications/templatetags/__init__.py
Normal file
0
allianceauth/notifications/templatetags/__init__.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Templatetags for notifications
|
||||
|
||||
These template tags are required to enable the notifications refresh functionality
|
||||
in the browser.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allianceauth.notifications.models import Notification
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def user_unread_notification_count(user: object) -> int:
|
||||
"""returns the number of unread notifications for user
|
||||
|
||||
Will return -1 on error
|
||||
"""
|
||||
if not isinstance(user, User):
|
||||
unread_count = -1
|
||||
else:
|
||||
unread_count = Notification.objects.user_unread_count(user.pk)
|
||||
|
||||
return unread_count
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def notifications_refresh_time() -> int:
|
||||
refresh_time = getattr(settings, 'NOTIFICATIONS_REFRESH_TIME', None)
|
||||
if (
|
||||
refresh_time is None or not isinstance(refresh_time, int) or refresh_time < 0
|
||||
):
|
||||
logger.warning('NOTIFICATIONS_REFRESH_TIME setting is invalid. Using default.')
|
||||
refresh_time = Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT
|
||||
|
||||
return refresh_time
|
28
allianceauth/notifications/tests/test_init.py
Normal file
28
allianceauth/notifications/tests/test_init.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.test import TestCase
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from .. import notify
|
||||
from ..models import Notification
|
||||
|
||||
MODULE_PATH = 'allianceauth.notifications'
|
||||
|
||||
|
||||
class TestUserNotificationCount(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user,
|
||||
'Magic Mike',
|
||||
'1',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
|
||||
def test_can_notify(self):
|
||||
notify(self.user, 'dummy')
|
||||
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
206
allianceauth/notifications/tests/test_managers.py
Normal file
206
allianceauth/notifications/tests/test_managers.py
Normal file
@ -0,0 +1,206 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from ..models import Notification
|
||||
|
||||
|
||||
MODULE_PATH = 'allianceauth.notifications.models'
|
||||
|
||||
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 42
|
||||
|
||||
|
||||
class TestQuerySet(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user_1 = AuthUtils.create_user('Peter Parker')
|
||||
cls.user_2 = AuthUtils.create_user('Clark Kent')
|
||||
|
||||
@patch(MODULE_PATH + '.Notification.objects.invalidate_user_notification_cache')
|
||||
def test_update_will_invalidate_cache(
|
||||
self, mock_invalidate_user_notification_cache
|
||||
):
|
||||
Notification.objects.notify_user(self.user_1, 'dummy_1')
|
||||
Notification.objects.notify_user(self.user_2, 'dummy_2')
|
||||
Notification.objects.update(viewed=True)
|
||||
self.assertEquals(mock_invalidate_user_notification_cache.call_count, 2)
|
||||
|
||||
|
||||
class TestUserNotify(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user,
|
||||
'Magic Mike',
|
||||
'1',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
|
||||
def test_can_notify(self):
|
||||
title = 'dummy_title'
|
||||
message = 'dummy message'
|
||||
level = 'danger'
|
||||
Notification.objects.notify_user(self.user, title, message, level)
|
||||
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||
obj = Notification.objects.first()
|
||||
self.assertEqual(obj.user, self.user)
|
||||
self.assertEqual(obj.title, title)
|
||||
self.assertEqual(obj.message, message)
|
||||
self.assertEqual(obj.level, level)
|
||||
|
||||
def test_use_message_as_title_if_missing(self):
|
||||
title = 'dummy_title'
|
||||
Notification.objects.notify_user(self.user, title)
|
||||
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||
obj = Notification.objects.first()
|
||||
self.assertEqual(obj.user, self.user)
|
||||
self.assertEqual(obj.title, title)
|
||||
self.assertEqual(obj.message, title)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=3)
|
||||
def test_remove_when_too_many_notifications(self):
|
||||
Notification.objects.notify_user(self.user, 'dummy')
|
||||
obj_2 = Notification.objects.notify_user(self.user, 'dummy')
|
||||
obj_3 = Notification.objects.notify_user(self.user, 'dummy')
|
||||
obj_4 = Notification.objects.notify_user(self.user, 'dummy')
|
||||
expected = {obj_2.pk, obj_3.pk, obj_4.pk}
|
||||
result = set(
|
||||
Notification.objects.filter(user=self.user).values_list("pk", flat=True)
|
||||
)
|
||||
self.assertSetEqual(result, expected)
|
||||
obj_5 = Notification.objects.notify_user(self.user, 'dummy')
|
||||
expected = {obj_3.pk, obj_4.pk, obj_5.pk}
|
||||
result = set(
|
||||
Notification.objects.filter(user=self.user).values_list("pk", flat=True)
|
||||
)
|
||||
self.assertSetEqual(result, expected)
|
||||
|
||||
|
||||
@patch(
|
||||
MODULE_PATH + '.Notification.NOTIFICATIONS_MAX_PER_USER_DEFAULT',
|
||||
NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
)
|
||||
class TestMaxNotificationsPerUser(TestCase):
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=None)
|
||||
def test_reset_to_default_if_not_defined(self):
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER='11')
|
||||
def test_reset_to_default_if_not_int(self):
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=-1)
|
||||
def test_reset_to_default_if_lt_zero(self):
|
||||
result = Notification.objects._max_notifications_per_user()
|
||||
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
|
||||
@patch('allianceauth.notifications.managers.cache')
|
||||
class TestUnreadCount(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user_1 = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user_1,
|
||||
'Magic Mike',
|
||||
'1',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
|
||||
# test notifications for mike
|
||||
Notification.objects.all().delete()
|
||||
Notification.objects.create(
|
||||
user=cls.user_1,
|
||||
level="INFO",
|
||||
title="Job 1 Failed",
|
||||
message="Because it was broken",
|
||||
viewed=True
|
||||
)
|
||||
Notification.objects.create(
|
||||
user=cls.user_1,
|
||||
level="INFO",
|
||||
title="Job 2 Failed",
|
||||
message="Because it was broken"
|
||||
)
|
||||
Notification.objects.create(
|
||||
user=cls.user_1,
|
||||
level="INFO",
|
||||
title="Job 3 Failed",
|
||||
message="Because it was broken"
|
||||
)
|
||||
|
||||
cls.user_2 = AuthUtils.create_user('teh_kid')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user_2,
|
||||
'The Kid', '2',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
|
||||
# Notifications for kid
|
||||
Notification.objects.create(
|
||||
user=cls.user_2,
|
||||
level="INFO",
|
||||
title="Job 6 Failed",
|
||||
message="Because it was broken"
|
||||
)
|
||||
|
||||
def test_update_cache_when_not_in_cache(self, mock_cache):
|
||||
mock_cache.get.return_value = None
|
||||
|
||||
result = Notification.objects.user_unread_count(self.user_1.pk)
|
||||
expected = 2
|
||||
self.assertEqual(result, expected)
|
||||
self.assertTrue(mock_cache.set.called)
|
||||
args, kwargs = mock_cache.set.call_args
|
||||
self.assertEqual(
|
||||
kwargs['key'],
|
||||
Notification.objects._user_notification_cache_key(self.user_1.pk)
|
||||
)
|
||||
self.assertEqual(kwargs['value'], expected)
|
||||
|
||||
def test_return_from_cache_when_in_cache(self, mock_cache):
|
||||
mock_cache.get.return_value = 42
|
||||
result = Notification.objects.user_unread_count(self.user_1.pk)
|
||||
expected = 42
|
||||
self.assertEqual(result, expected)
|
||||
self.assertFalse(mock_cache.set.called)
|
||||
|
||||
def test_return_error_code_when_user_not_found(self, mock_cache):
|
||||
mock_cache.get.return_value = None
|
||||
invalid_user_id = max([user.pk for user in User.objects.all()]) + 1
|
||||
result = Notification.objects.user_unread_count(invalid_user_id)
|
||||
expected = -1
|
||||
self.assertEqual(result, expected)
|
||||
self.assertFalse(mock_cache.set.called)
|
||||
|
||||
def test_can_invalidate_cache(self, mock_cache):
|
||||
Notification.objects.invalidate_user_notification_cache(self.user_1.pk)
|
||||
self.assertTrue(mock_cache.delete)
|
||||
args, kwargs = mock_cache.delete.call_args
|
||||
self.assertEqual(
|
||||
kwargs['key'],
|
||||
Notification.objects._user_notification_cache_key(self.user_1.pk)
|
||||
)
|
73
allianceauth/notifications/tests/test_models.py
Normal file
73
allianceauth/notifications/tests/test_models.py
Normal file
@ -0,0 +1,73 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..models import Notification
|
||||
|
||||
|
||||
MODULE_PATH = 'allianceauth.notifications.models'
|
||||
|
||||
|
||||
class TestUserNotify(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user,
|
||||
'Magic Mike',
|
||||
'1',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
|
||||
@patch(MODULE_PATH + '.Notification.objects.invalidate_user_notification_cache')
|
||||
def test_save_will_invalidate_cache(self, mock_invalidate_user_notification_cache):
|
||||
obj = Notification.objects.notify_user(self.user, 'dummy')
|
||||
self.assertTrue(Notification.objects.filter(pk=obj.pk).exists())
|
||||
self.assertEquals(mock_invalidate_user_notification_cache.call_count, 1)
|
||||
|
||||
@patch(MODULE_PATH + '.Notification.objects.invalidate_user_notification_cache')
|
||||
def test_delete_will_invalidate_cache(
|
||||
self, mock_invalidate_user_notification_cache
|
||||
):
|
||||
obj = Notification.objects.notify_user(self.user, 'dummy')
|
||||
obj.delete()
|
||||
self.assertFalse(Notification.objects.filter(pk=obj.pk).exists())
|
||||
self.assertEquals(mock_invalidate_user_notification_cache.call_count, 2)
|
||||
|
||||
def test_can_view(self):
|
||||
obj = Notification.objects.notify_user(self.user, 'dummy')
|
||||
self.assertFalse(obj.viewed)
|
||||
obj.mark_viewed()
|
||||
obj.refresh_from_db()
|
||||
self.assertTrue(obj.viewed)
|
||||
|
||||
def test_can_set_level(self):
|
||||
obj = Notification.objects.notify_user(self.user, 'dummy', level='info')
|
||||
obj.set_level('ERROR')
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.level, 'danger')
|
||||
|
||||
obj.set_level('CRITICAL')
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.level, 'danger')
|
||||
|
||||
obj.set_level('WARN')
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.level, 'warning')
|
||||
|
||||
obj.set_level('INFO')
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.level, 'info')
|
||||
|
||||
obj.set_level('DEBUG')
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.level, 'success')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
obj.set_level('XXX')
|
@ -1,76 +0,0 @@
|
||||
from unittest import mock
|
||||
from django.test import TestCase
|
||||
from allianceauth.notifications.context_processors import user_notification_count
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from django.core.cache import cache
|
||||
from allianceauth.notifications.models import Notification
|
||||
|
||||
class TestNotificationCount(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(cls.user, 'Magic Mike', '1', corp_id='2', corp_name='Pole Riders', corp_ticker='PRIDE', alliance_id='3', alliance_name='RIDERS')
|
||||
cls.user.profile.refresh_from_db()
|
||||
|
||||
### test notifications for mike
|
||||
Notification.objects.all().delete()
|
||||
Notification.objects.create(user=cls.user,
|
||||
level="INFO",
|
||||
title="Job 1 Failed",
|
||||
message="Because it was broken",
|
||||
viewed=True)
|
||||
Notification.objects.create(user=cls.user,
|
||||
level="INFO",
|
||||
title="Job 2 Failed",
|
||||
message="Because it was broken")
|
||||
Notification.objects.create(user=cls.user,
|
||||
level="INFO",
|
||||
title="Job 3 Failed",
|
||||
message="Because it was broken")
|
||||
Notification.objects.create(user=cls.user,
|
||||
level="INFO",
|
||||
title="Job 4 Failed",
|
||||
message="Because it was broken")
|
||||
Notification.objects.create(user=cls.user,
|
||||
level="INFO",
|
||||
title="Job 5 Failed",
|
||||
message="Because it was broken")
|
||||
Notification.objects.create(user=cls.user,
|
||||
level="INFO",
|
||||
title="Job 6 Failed",
|
||||
message="Because it was broken")
|
||||
|
||||
cls.user2 = AuthUtils.create_user('teh_kid')
|
||||
AuthUtils.add_main_character(cls.user, 'The Kid', '2', corp_id='2', corp_name='Pole Riders', corp_ticker='PRIDE', alliance_id='3', alliance_name='RIDERS')
|
||||
cls.user2.profile.refresh_from_db()
|
||||
|
||||
# Noitification for kid
|
||||
Notification.objects.create(user=cls.user2,
|
||||
level="INFO",
|
||||
title="Job 6 Failed",
|
||||
message="Because it was broken")
|
||||
|
||||
|
||||
def test_no_cache(self):
|
||||
mock_req = mock.MagicMock()
|
||||
mock_req.user.id = self.user.id
|
||||
|
||||
cache.delete("u-note:{}".format(self.user.id)) # force the db to be hit
|
||||
context_dict = user_notification_count(mock_req)
|
||||
self.assertIsInstance(context_dict, dict)
|
||||
self.assertEqual(context_dict.get('notifications'), 5) # 5 only
|
||||
|
||||
|
||||
@mock.patch('allianceauth.notifications.models.Notification.objects')
|
||||
def test_cache(self, mock_foo):
|
||||
mock_foo.filter.return_value = mock_foo
|
||||
mock_foo.count.return_value = 5
|
||||
mock_req = mock.MagicMock()
|
||||
mock_req.user.id = self.user.id
|
||||
|
||||
cache.set("u-note:{}".format(self.user.id),10,5)
|
||||
context_dict = user_notification_count(mock_req)
|
||||
self.assertIsInstance(context_dict, dict)
|
||||
self.assertEqual(context_dict.get('notifications'), 10) # cached value
|
||||
self.assertEqual(mock_foo.called, 0) # ensure the DB was not hit
|
86
allianceauth/notifications/tests/test_templatetags.py
Normal file
86
allianceauth/notifications/tests/test_templatetags.py
Normal file
@ -0,0 +1,86 @@
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from ..templatetags.auth_notifications import (
|
||||
user_unread_notification_count, notifications_refresh_time
|
||||
)
|
||||
|
||||
MODULE_PATH = 'allianceauth.notifications.templatetags.auth_notifications'
|
||||
|
||||
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 66
|
||||
MY_NOTIFICATIONS_REFRESH_TIME = 23
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.Notification.objects.user_unread_count')
|
||||
class TestUserNotificationCount(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user,
|
||||
'Magic Mike',
|
||||
'1',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
|
||||
def test_return_normal(self, mock_user_unread_count):
|
||||
unread_count = 42
|
||||
mock_user_unread_count.return_value = unread_count
|
||||
|
||||
result = user_unread_notification_count(self.user)
|
||||
expected = unread_count
|
||||
self.assertEqual(result, expected)
|
||||
args, kwargs = mock_user_unread_count.call_args
|
||||
self.assertEqual(args[0], self.user.pk)
|
||||
|
||||
def test_return_error_if_non_user(self, mock_user_unread_count):
|
||||
unread_count = -1
|
||||
mock_user_unread_count.return_value = unread_count
|
||||
|
||||
result = user_unread_notification_count('invalid')
|
||||
expected = unread_count
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
@patch(
|
||||
MODULE_PATH + '.Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT',
|
||||
NOTIFICATIONS_REFRESH_TIME_DEFAULT
|
||||
)
|
||||
class TestNotificationsRefreshTime(TestCase):
|
||||
|
||||
@override_settings(NOTIFICATIONS_REFRESH_TIME=MY_NOTIFICATIONS_REFRESH_TIME)
|
||||
def test_return_from_setting(self):
|
||||
result = notifications_refresh_time()
|
||||
expected = MY_NOTIFICATIONS_REFRESH_TIME
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@override_settings(NOTIFICATIONS_REFRESH_TIME=0)
|
||||
def test_refresh_time_can_be_zero(self):
|
||||
result = notifications_refresh_time()
|
||||
expected = 0
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@override_settings(NOTIFICATIONS_REFRESH_TIME=None)
|
||||
def test_return_default_refresh_time_if_not_exists(self):
|
||||
result = notifications_refresh_time()
|
||||
expected = NOTIFICATIONS_REFRESH_TIME_DEFAULT
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@override_settings(NOTIFICATIONS_REFRESH_TIME='33')
|
||||
def test_return_default_refresh_time_if_not_int(self):
|
||||
result = notifications_refresh_time()
|
||||
expected = NOTIFICATIONS_REFRESH_TIME_DEFAULT
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@override_settings(NOTIFICATIONS_REFRESH_TIME=-1)
|
||||
def test_return_default_refresh_time_if_lt_0(self):
|
||||
result = notifications_refresh_time()
|
||||
expected = NOTIFICATIONS_REFRESH_TIME_DEFAULT
|
||||
self.assertEqual(result, expected)
|
49
allianceauth/notifications/tests/test_views.py
Normal file
49
allianceauth/notifications/tests/test_views.py
Normal file
@ -0,0 +1,49 @@
|
||||
import json
|
||||
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.urls import reverse
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..views import user_notifications_count
|
||||
|
||||
|
||||
MODULE_PATH = 'allianceauth.notifications.views'
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('magic_mike')
|
||||
AuthUtils.add_main_character(
|
||||
cls.user,
|
||||
'Magic Mike',
|
||||
'1',
|
||||
corp_id='2',
|
||||
corp_name='Pole Riders',
|
||||
corp_ticker='PRIDE',
|
||||
alliance_id='3',
|
||||
alliance_name='RIDERS'
|
||||
)
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
@patch(MODULE_PATH + '.Notification.objects.user_unread_count')
|
||||
def test_user_notifications_count(self, mock_user_unread_count):
|
||||
unread_count = 42
|
||||
user_pk = 3
|
||||
mock_user_unread_count.return_value = unread_count
|
||||
|
||||
request = self.factory.get(
|
||||
reverse('notifications:user_notifications_count', args=[user_pk])
|
||||
)
|
||||
request.user = self.user
|
||||
|
||||
response = user_notifications_count(request, user_pk)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_user_unread_count.called)
|
||||
expected = {'unread_count': unread_count}
|
||||
result = json.loads(response.content.decode(response.charset))
|
||||
self.assertDictEqual(result, expected)
|
@ -9,4 +9,9 @@ urlpatterns = [
|
||||
url(r'^notifications/delete_all_read/$', views.delete_all_read, name='delete_all_read'),
|
||||
url(r'^notifications/$', views.notification_list, name='list'),
|
||||
url(r'^notifications/(\w+)/$', views.notification_view, name='view'),
|
||||
url(
|
||||
r'^user_notifications_count/(?P<user_pk>\d+)/$',
|
||||
views.user_notifications_count,
|
||||
name='user_notifications_count'
|
||||
),
|
||||
]
|
||||
|
@ -1,9 +1,12 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from .models import Notification
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -11,9 +14,15 @@ logger = logging.getLogger(__name__)
|
||||
@login_required
|
||||
def notification_list(request):
|
||||
logger.debug("notification_list called by user %s" % request.user)
|
||||
new_notifs = Notification.objects.filter(user=request.user).filter(viewed=False)
|
||||
old_notifs = Notification.objects.filter(user=request.user).filter(viewed=True)
|
||||
logger.debug("User %s has %s unread and %s read notifications" % (request.user, len(new_notifs), len(old_notifs)))
|
||||
notifications_qs = Notification.objects.filter(user=request.user).order_by("-timestamp")
|
||||
new_notifs = notifications_qs.filter(viewed=False)
|
||||
old_notifs = notifications_qs.filter(viewed=True)
|
||||
logger.debug(
|
||||
"User %s has %s unread and %s read notifications",
|
||||
request.user,
|
||||
len(new_notifs),
|
||||
len(old_notifs)
|
||||
)
|
||||
context = {
|
||||
'read': old_notifs,
|
||||
'unread': new_notifs,
|
||||
@ -23,39 +32,53 @@ def notification_list(request):
|
||||
|
||||
@login_required
|
||||
def notification_view(request, notif_id):
|
||||
logger.debug("notification_view called by user %s for notif_id %s" % (request.user, notif_id))
|
||||
logger.debug(
|
||||
"notification_view called by user %s for notif_id %s",
|
||||
request.user,
|
||||
notif_id
|
||||
)
|
||||
notif = get_object_or_404(Notification, pk=notif_id)
|
||||
if notif.user == request.user:
|
||||
logger.debug("Providing notification for user %s" % request.user)
|
||||
logger.debug("Providing notification for user %s", request.user)
|
||||
context = {'notif': notif}
|
||||
notif.view()
|
||||
notif.mark_viewed()
|
||||
return render(request, 'notifications/view.html', context)
|
||||
else:
|
||||
logger.warn(
|
||||
"User %s not authorized to view notif_id %s belonging to user %s" % (request.user, notif_id, notif.user))
|
||||
"User %s not authorized to view notif_id %s belonging to user %s",
|
||||
request.user,
|
||||
notif_id, notif.user
|
||||
)
|
||||
messages.error(request, _('You are not authorized to view that notification.'))
|
||||
return redirect('notifications:list')
|
||||
|
||||
|
||||
@login_required
|
||||
def remove_notification(request, notif_id):
|
||||
logger.debug("remove notification called by user %s for notif_id %s" % (request.user, notif_id))
|
||||
logger.debug(
|
||||
"remove notification called by user %s for notif_id %s",
|
||||
request.user,
|
||||
notif_id
|
||||
)
|
||||
notif = get_object_or_404(Notification, pk=notif_id)
|
||||
if notif.user == request.user:
|
||||
if Notification.objects.filter(id=notif_id).exists():
|
||||
notif.delete()
|
||||
logger.info("Deleting notif id %s by user %s" % (notif_id, request.user))
|
||||
logger.info("Deleting notif id %s by user %s", notif_id, request.user)
|
||||
messages.success(request, _('Deleted notification.'))
|
||||
else:
|
||||
logger.error(
|
||||
"Unable to delete notif id %s for user %s - notif matching id not found." % (notif_id, request.user))
|
||||
"Unable to delete notif id %s for user %s - notif matching id not found.",
|
||||
notif_id,
|
||||
request.user
|
||||
)
|
||||
messages.error(request, _('Failed to locate notification.'))
|
||||
return redirect('notifications:list')
|
||||
|
||||
|
||||
@login_required
|
||||
def mark_all_read(request):
|
||||
logger.debug('mark all notifications read called by user %s' % request.user)
|
||||
logger.debug('mark all notifications read called by user %s', request.user)
|
||||
Notification.objects.filter(user=request.user).update(viewed=True)
|
||||
messages.success(request, _('Marked all notifications as read.'))
|
||||
return redirect('notifications:list')
|
||||
@ -63,7 +86,17 @@ def mark_all_read(request):
|
||||
|
||||
@login_required
|
||||
def delete_all_read(request):
|
||||
logger.debug('delete all read notifications called by user %s' % request.user)
|
||||
logger.debug('delete all read notifications called by user %s', request.user)
|
||||
Notification.objects.filter(user=request.user).filter(viewed=True).delete()
|
||||
messages.success(request, _('Deleted all read notifications.'))
|
||||
return redirect('notifications:list')
|
||||
|
||||
|
||||
def user_notifications_count(request, user_pk: int):
|
||||
"""returns to notifications count for the give user as JSON
|
||||
|
||||
This view is public and does not require login
|
||||
"""
|
||||
unread_count = Notification.objects.user_unread_count(user_pk)
|
||||
data = {'unread_count': unread_count}
|
||||
return JsonResponse(data, safe=False)
|
||||
|
@ -105,7 +105,6 @@ TEMPLATES = [
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.static',
|
||||
'django.template.context_processors.tz',
|
||||
'allianceauth.notifications.context_processors.user_notification_count',
|
||||
'allianceauth.context_processors.auth_settings',
|
||||
],
|
||||
},
|
||||
@ -173,6 +172,8 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ['*']
|
||||
DATABASES = {
|
||||
|
75
allianceauth/static/js/refresh_notifications.js
Normal file
75
allianceauth/static/js/refresh_notifications.js
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
This script refreshed the unread notification count in the top menu
|
||||
on a regular basis so to keep the user apprised about newly arrived
|
||||
notifications without having to reload the page.
|
||||
|
||||
The refresh rate can be changes via the Django setting NOTIFICATIONS_REFRESH_TIME.
|
||||
See documentation for details.
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
var elem = document.getElementById("dataExport");
|
||||
var notificationsListViewUrl = elem.getAttribute("data-notificationsListViewUrl");
|
||||
var notificationsRefreshTime = elem.getAttribute("data-notificationsRefreshTime");
|
||||
var userNotificationsCountViewUrl = elem.getAttribute(
|
||||
"data-userNotificationsCountViewUrl"
|
||||
);
|
||||
|
||||
// update the notification unread count in the top menu
|
||||
function update_notifications() {
|
||||
$.getJSON(userNotificationsCountViewUrl, function (data, status) {
|
||||
if (status == 'success') {
|
||||
var innerHtml = "";
|
||||
var unread_count = data.unread_count;
|
||||
if (unread_count > 0) {
|
||||
innerHtml = (
|
||||
`Notifications <span class="badge">${unread_count}</span>`
|
||||
)
|
||||
}
|
||||
else {
|
||||
innerHtml = '<i class="far fa-bell"></i>'
|
||||
}
|
||||
$("#menu_item_notifications").html(
|
||||
`<a href="${notificationsListViewUrl}">${innerHtml}</a>`
|
||||
);
|
||||
}
|
||||
else {
|
||||
console.error(
|
||||
`Failed to load HTMl to render notifications item. Error: `
|
||||
`${xhr.status}': '${xhr.statusText}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var myInterval;
|
||||
|
||||
// activate automatic refreshing every x seconds
|
||||
function activate_refreshing() {
|
||||
if (notificationsRefreshTime > 0) {
|
||||
myInterval = setInterval(
|
||||
update_notifications, notificationsRefreshTime * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// deactivate automatic refreshing
|
||||
function deactivate_refreshing() {
|
||||
if ((notificationsRefreshTime > 0) && (typeof myInterval !== 'undefined')) {
|
||||
clearInterval(myInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// refreshing only happens on active browser tab
|
||||
$(document).on({
|
||||
'show': function () {
|
||||
activate_refreshing()
|
||||
},
|
||||
'hide': function () {
|
||||
deactivate_refreshing()
|
||||
}
|
||||
});
|
||||
|
||||
// Initial start of refreshing on script loading
|
||||
activate_refreshing()
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load navactive %}
|
||||
{% load auth_notifications %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -9,7 +10,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
{% include 'allianceauth/icons.html' %}
|
||||
|
||||
<title>{% block title %}{% block page_title %}{% endblock page_title %} - Alliance Auth{% endblock title %}</title>
|
||||
@ -27,7 +27,6 @@
|
||||
<div id="wrapper" class="container">
|
||||
<!-- Navigation -->
|
||||
{% include 'allianceauth/top-menu.html' %}
|
||||
|
||||
<div class="row" id="site-body-wrapper">
|
||||
{% include 'allianceauth/side-menu.html' %}
|
||||
<div class="col-sm-10">
|
||||
@ -40,7 +39,6 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
@ -48,12 +46,20 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- share data with JS part -->
|
||||
<div
|
||||
id="dataExport"
|
||||
data-notificationsListViewUrl="{% url 'notifications:list' %}"
|
||||
data-notificationsRefreshTime="{% notifications_refresh_time %}"
|
||||
data-userNotificationsCountViewUrl="{% url 'notifications:user_notifications_count' request.user.pk %}"
|
||||
>
|
||||
</div>
|
||||
{% include 'bundles/bootstrap-js.html' %}
|
||||
{% include 'bundles/jquery-visibility-js.html' %}
|
||||
<script src="{% static 'js/refresh_notifications.js' %}"></script>
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
<script>
|
||||
{% block extra_script %}
|
||||
{% endblock extra_script %}
|
||||
|
@ -0,0 +1,13 @@
|
||||
{% load auth_notifications %}
|
||||
|
||||
{% with unread_count=request.user|user_unread_notification_count %}
|
||||
{% if unread_count > 0 %}
|
||||
<a href="{% url 'notifications:list' %}">Notifications
|
||||
<span class="badge">{{ unread_count }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'notifications:list' %}">
|
||||
<i class="far fa-bell"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
@ -20,17 +20,11 @@
|
||||
<li>
|
||||
{% include 'allianceauth/night-toggle.html' %}
|
||||
</li>
|
||||
{% if notifications %}
|
||||
<li class="{% navactive request 'notifications:' %}">
|
||||
<a href="{% url 'notifications:list' %}">Notifications
|
||||
<span class="badge">{{ notifications }}</span>
|
||||
</a>
|
||||
<li
|
||||
class="{% navactive request 'notifications:' %}" id="menu_item_notifications"
|
||||
>
|
||||
{% include 'allianceauth/notifications_menu_item.html' %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'notifications:list' %}">
|
||||
<i class="far fa-bell"></i></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.is_staff %}
|
||||
<li><a href="{% url 'admin:index' %}">{% trans "Admin" %}</a></li>
|
||||
@ -64,3 +58,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
4
allianceauth/templates/bundles/jquery-visibility-js.html
Normal file
4
allianceauth/templates/bundles/jquery-visibility-js.html
Normal file
@ -0,0 +1,4 @@
|
||||
{% load static %}
|
||||
<!-- Start jQuery visibility js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-visibility/1.0.11/jquery-visibility.min.js"></script>
|
||||
<!-- End jQuery visibility js -->
|
BIN
docs/_static/images/features/core/notifications.png
vendored
Normal file
BIN
docs/_static/images/features/core/notifications.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
@ -9,5 +9,6 @@ To reduce redundancy and help speed up development we encourage developers to ut
|
||||
esi
|
||||
evelinks
|
||||
eveonline
|
||||
notifications
|
||||
testutils
|
||||
```
|
||||
|
25
docs/development/tech_docu/api/notifications.rst
Normal file
25
docs/development/tech_docu/api/notifications.rst
Normal file
@ -0,0 +1,25 @@
|
||||
======================
|
||||
notifications
|
||||
======================
|
||||
|
||||
The notifications package has an API for sending notifications.
|
||||
|
||||
Location: ``allianceauth.notifications``
|
||||
|
||||
.. automodule:: allianceauth.notifications.__init__
|
||||
:members: notify
|
||||
:undoc-members:
|
||||
|
||||
models
|
||||
===========
|
||||
|
||||
.. autoclass:: allianceauth.notifications.models.Notification
|
||||
:members: view, set_level, LEVEL_CHOICES
|
||||
:undoc-members:
|
||||
|
||||
managers
|
||||
===========
|
||||
|
||||
.. autoclass:: allianceauth.notifications.managers.NotificationManager
|
||||
:members: notify_user, user_unread_count
|
||||
:undoc-members:
|
@ -10,4 +10,5 @@ Managing access to applications and services is one of the core functions of **A
|
||||
states
|
||||
groups
|
||||
groupmanagement
|
||||
notifications
|
||||
```
|
||||
|
14
docs/features/core/notifications.md
Normal file
14
docs/features/core/notifications.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Notifications
|
||||
|
||||
Alliance Auth has a build in notification system. The purpose of the notification system is to provide an easy and quick way to send messages to users of Auth. For example some apps are using it to inform users about results after long running tasks have completed and admins will automatically get notifications about system errors.
|
||||
|
||||
The number of unread notifications is shown to the user in the top menu. And the user can click on the notification count to open the notifications app.
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
|
||||
The notifications app can be configured through settings.
|
||||
|
||||
- `NOTIFICATIONS_REFRESH_TIME`: The unread count in the top menu is automatically refreshed to keep the user informed about new notifications. This setting allows to set the time between each refresh in seconds. You can also set it to `0` to turn off automatic refreshing. Default: `30`
|
||||
- `NOTIFICATIONS_MAX_PER_USER`: Maximum number of notifications that are stored per user. Older notifications are replaced by newer once. Default: `50`
|
Loading…
x
Reference in New Issue
Block a user