Compare commits

...

10 Commits

Author SHA1 Message Date
Erik Kalkoken
1606ea7c7d Merge branch 'seperate-out-celery-once' into 'master'
Remove dependecies from celery_once customizations

See merge request allianceauth/allianceauth!1491
2025-10-15 21:13:52 +02:00
Ariel Rin
92a1bd40a3 Merge branch 'inline-js-overhaul' into 'master'
[CHANGE] InlineJS overhaul

See merge request allianceauth/allianceauth!1766
2025-10-10 09:54:36 +00:00
Ariel Rin
b8bbd0d1c1 Merge branch 'fix-deprecated-utcnow' into 'master'
[FIX] Calls to deprecated `utcnow` method of `datetime.datetime`

See merge request allianceauth/allianceauth!1767
2025-10-10 09:52:45 +00:00
Peter Pfeufer
b7a6c9379a
Merge remote-tracking branch 'origin/fix-deprecated-utcnow' into fix-deprecated-utcnow 2025-09-27 07:51:53 +02:00
Peter Pfeufer
e5f3a67919
[FIX] Calls to deprecated utcnow method of datetime.datetime
datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).

```python
@classmethod
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.UTC)")
def utcnow(cls) -> Self
```
2025-09-27 07:51:46 +02:00
Peter Pfeufer
466113e6cb
[FIX] Calls to deprecated utcnow method of datetime.datetime
datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).

```python
@classmethod
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.UTC)")
def utcnow(cls) -> Self
```
2025-09-26 17:20:40 +02:00
Peter Pfeufer
b1f5aad9f9
[CHANGE] Move inline JS into $(document).ready(()) block 2025-09-25 09:38:20 +02:00
Peter Pfeufer
464016ac05
[CHANGE] Move task queue update JS into its own file 2025-09-25 09:14:03 +02:00
Peter Pfeufer
cc76f09a6e
[CHANGE Move sidebar collapse JS into its own file 2025-09-25 08:11:17 +02:00
Erik Kalkoken
0dd47e72bc Move celery once config into own package 2023-02-28 15:16:51 +01:00
22 changed files with 519 additions and 483 deletions

View File

@ -27,7 +27,7 @@ def dashboard_results(hours: int) -> _TaskCounts:
my_earliest = events.first_event(earliest=earliest) my_earliest = events.first_event(earliest=earliest)
return [my_earliest] if my_earliest else [] return [my_earliest] if my_earliest else []
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours) earliest = dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=hours)
earliest_events = [] earliest_events = []
succeeded_count = succeeded_tasks.count(earliest=earliest) succeeded_count = succeeded_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded_tasks, earliest) earliest_events += earliest_if_exists(succeeded_tasks, earliest)

View File

@ -42,7 +42,7 @@ class EventSeries:
- event_time: timestamp of event. Will use current time if not specified. - event_time: timestamp of event. Will use current time if not specified.
""" """
if not event_time: if not event_time:
event_time = dt.datetime.utcnow() event_time = dt.datetime.now(dt.timezone.utc)
my_id = self._redis.incr(self._key_counter) my_id = self._redis.incr(self._key_counter)
self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()}) self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})

View File

@ -65,6 +65,7 @@
{% include 'bundles/timers-js.html' %} {% include 'bundles/timers-js.html' %}
<script> <script>
$(document).ready(() => {
// Data // Data
const timers = [ const timers = [
{% for op in optimer %} {% for op in optimer %}
@ -143,5 +144,6 @@
// Start timed updates // Start timed updates
setInterval(timedUpdate, 1000); setInterval(timedUpdate, 1000);
});
</script> </script>
{% endblock content %} {% endblock content %}

View File

@ -26,7 +26,7 @@ app.conf.task_default_priority = 5 # anything called with the task.delay() will
app.conf.worker_prefetch_multiplier = 1 # only prefetch single tasks at a time on the workers so that prio tasks happen app.conf.worker_prefetch_multiplier = 1 # only prefetch single tasks at a time on the workers so that prio tasks happen
app.conf.ONCE = { app.conf.ONCE = {
'backend': 'allianceauth.services.tasks.DjangoBackend', 'backend': 'allianceauth.services.celery_once.backends.DjangoBackend',
'settings': {} 'settings': {}
} }

View File

@ -0,0 +1,19 @@
from celery_once import AlreadyQueued
from django.core.cache import cache
class DjangoBackend:
"""Locking backend for celery once."""
def __init__(self, settings):
pass
@staticmethod
def raise_or_lock(key, timeout):
acquired = cache.add(key=key, value="lock", timeout=timeout)
if not acquired:
raise AlreadyQueued(int(cache.ttl(key)))
@staticmethod
def clear_lock(key):
return cache.delete(key)

View File

@ -0,0 +1,8 @@
from celery_once import QueueOnce as BaseTask
class QueueOnce(BaseTask):
"""QueueOnce class with custom defaults."""
once = BaseTask.once
once["graceful"] = True

View File

@ -0,0 +1,47 @@
from celery_once import AlreadyQueued
from django.core.cache import cache
from django.test import TestCase
from allianceauth.services.celery_once.backends import DjangoBackend
class TestDjangoBackend(TestCase):
TEST_KEY = "my-django-backend-test-key"
TIMEOUT = 1800
def setUp(self) -> None:
cache.delete(self.TEST_KEY)
self.backend = DjangoBackend(dict())
def test_can_get_lock(self):
"""
when lock can be acquired
then set it with timeout
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.assertIsNotNone(cache.get(self.TEST_KEY))
self.assertAlmostEqual(cache.ttl(self.TEST_KEY), self.TIMEOUT, delta=2)
def test_when_cant_get_lock_raise_exception(self):
"""
when lock can bot be acquired
then raise AlreadyQueued exception with countdown
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
try:
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
except Exception as ex:
self.assertIsInstance(ex, AlreadyQueued)
self.assertAlmostEqual(ex.countdown, self.TIMEOUT, delta=2)
def test_can_clear_lock(self):
"""
when a lock exists
then can get a new lock after clearing it
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.backend.clear_lock(self.TEST_KEY)
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.assertIsNotNone(cache.get(self.TEST_KEY))

View File

@ -100,13 +100,13 @@ def jabber_broadcast_view(request):
if main_char is not None: if main_char is not None:
message_to_send = form.cleaned_data['message'] + "\n##### SENT BY: " + "[" + main_char.corporation_ticker + "]" + \ message_to_send = form.cleaned_data['message'] + "\n##### SENT BY: " + "[" + main_char.corporation_ticker + "]" + \
main_char.character_name + " TO: " + \ main_char.character_name + " TO: " + \
form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( form.cleaned_data['group'] + " WHEN: " + datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n"
group_to_send = form.cleaned_data['group'] group_to_send = form.cleaned_data['group']
else: else:
message_to_send = form.cleaned_data['message'] + "\n##### SENT BY: " + "No character but can send pings?" + " TO: " + \ message_to_send = form.cleaned_data['message'] + "\n##### SENT BY: " + "No character but can send pings?" + " TO: " + \
form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( form.cleaned_data['group'] + " WHEN: " + datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n"
group_to_send = form.cleaned_data['group'] group_to_send = form.cleaned_data['group']

View File

@ -2,7 +2,7 @@ import random
import string import string
import calendar import calendar
import re import re
from datetime import datetime import datetime as dt
from passlib.apps import phpbb3_context from passlib.apps import phpbb3_context
from django.db import connections from django.db import connections
@ -128,7 +128,7 @@ class Phpbb3Manager:
@staticmethod @staticmethod
def __get_current_utc_date(): def __get_current_utc_date():
d = datetime.utcnow() d = dt.datetime.now(dt.timezone.utc)
unixtime = calendar.timegm(d.utctimetuple()) unixtime = calendar.timegm(d.utctimetuple())
return unixtime return unixtime

View File

@ -1,7 +1,7 @@
import random import random
import string import string
import calendar import calendar
from datetime import datetime import datetime as dt
import hashlib import hashlib
import logging import logging
import re import re
@ -105,7 +105,7 @@ class SmfManager:
@staticmethod @staticmethod
def get_current_utc_date(): def get_current_utc_date():
d = datetime.utcnow() d = dt.datetime.now(dt.timezone.utc)
unixtime = calendar.timegm(d.utctimetuple()) unixtime = calendar.timegm(d.utctimetuple())
return unixtime return unixtime

View File

@ -3,33 +3,11 @@ import logging
from celery import shared_task from celery import shared_task
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .hooks import ServicesHook from .hooks import ServicesHook
from celery_once import QueueOnce as BaseTask, AlreadyQueued from .celery_once.tasks import QueueOnce # noqa: F401 - for backwards compatibility
from django.core.cache import cache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class QueueOnce(BaseTask):
once = BaseTask.once
once['graceful'] = True
class DjangoBackend:
def __init__(self, settings):
pass
@staticmethod
def raise_or_lock(key, timeout):
acquired = cache.add(key=key, value="lock", timeout=timeout)
if not acquired:
raise AlreadyQueued(int(cache.ttl(key)))
@staticmethod
def clear_lock(key):
return cache.delete(key)
@shared_task(bind=True) @shared_task(bind=True)
def validate_services(self, pk): def validate_services(self, pk):
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
@ -38,7 +16,7 @@ def validate_services(self, pk):
for svc in ServicesHook.get_services(): for svc in ServicesHook.get_services():
try: try:
svc.validate_user(user) svc.validate_user(user)
except: except Exception:
logger.exception(f'Exception running validate_user for services module {svc} on user {user}') logger.exception(f'Exception running validate_user for services module {svc} on user {user}')

View File

@ -1,15 +1,10 @@
from unittest import mock from unittest import mock
from celery_once import AlreadyQueued
from django.core.cache import cache
from django.test import override_settings, TestCase from django.test import override_settings, TestCase
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.services.tasks import validate_services, update_groups_for_user from allianceauth.services.tasks import validate_services, update_groups_for_user
from ..tasks import DjangoBackend
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True) @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class ServicesTasksTestCase(TestCase): class ServicesTasksTestCase(TestCase):
@ -46,46 +41,3 @@ class ServicesTasksTestCase(TestCase):
self.assertTrue(svc.update_groups.called) self.assertTrue(svc.update_groups.called)
args, _ = svc.update_groups.call_args args, _ = svc.update_groups.call_args
self.assertEqual(self.member, args[0]) # Assert correct user self.assertEqual(self.member, args[0]) # Assert correct user
class TestDjangoBackend(TestCase):
TEST_KEY = "my-django-backend-test-key"
TIMEOUT = 1800
def setUp(self) -> None:
cache.delete(self.TEST_KEY)
self.backend = DjangoBackend(dict())
def test_can_get_lock(self):
"""
when lock can be acquired
then set it with timetout
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.assertIsNotNone(cache.get(self.TEST_KEY))
self.assertAlmostEqual(cache.ttl(self.TEST_KEY), self.TIMEOUT, delta=2)
def test_when_cant_get_lock_raise_exception(self):
"""
when lock can bot be acquired
then raise AlreadyQueued exception with countdown
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
try:
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
except Exception as ex:
self.assertIsInstance(ex, AlreadyQueued)
self.assertAlmostEqual(ex.countdown, self.TIMEOUT, delta=2)
def test_can_clear_lock(self):
"""
when a lock exists
then can get a new lock after clearing it
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.backend.clear_lock(self.TEST_KEY)
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.assertIsNotNone(cache.get(self.TEST_KEY))

View File

@ -0,0 +1,207 @@
/* global fetchGet, numberFormatter, taskQueueSettings */
$(document).ready(() => {
'use strict';
const elements = {
total: document.getElementById('total-task-count'),
uptime: document.getElementById('celery-uptime'),
running: document.getElementById('running-task-count'),
queued: document.getElementById('queued-tasks-count'),
succeeded: document.getElementById('succeeded-tasks-count'),
retried: document.getElementById('retried-tasks-count'),
failed: document.getElementById('failed-tasks-count')
};
/**
* Fetches the task queue status and updates the UI elements accordingly.
* It retrieves the total number of tasks, running tasks, queued tasks,
* succeeded tasks, retried tasks, and failed tasks, and updates the
* corresponding HTML elements with the fetched data.
* It also updates the progress bars for succeeded, retried, and failed tasks.
* The function is called immediately and then every 30 seconds to keep the
* task queue status up to date.
*/
const updateTaskCount = () => {
fetchGet({url: taskQueueSettings.url})
.then((data) => {
const elemProgressBar = document.getElementById('celery-tasks-progress-bar');
const progressElements = {
succeeded: {
bar: document.getElementById('celery-progress-bar-succeeded'),
text: document.getElementById('celery-progress-bar-succeeded-progress')
},
retried: {
bar: document.getElementById('celery-progress-bar-retried'),
text: document.getElementById('celery-progress-bar-retried-progress')
},
failed: {
bar: document.getElementById('celery-progress-bar-failed'),
text: document.getElementById('celery-progress-bar-failed-progress')
}
};
// Assign progress data from the fetched data to variables
const {
earliest_task: earliestTask,
tasks_total: tasksTotal,
tasks_running: tasksRunning,
tasks_queued: tasksQueued,
tasks_succeeded: tasksSucceeded,
tasks_retried: tasksRetried,
tasks_failed: tasksFailed
} = data;
/**
* Updates the text content of the specified HTML element with the given value.
* If the value is null, it sets the text to 'N/A'.
* Otherwise, it formats the number using the locale-specific format.
*
* @param {HTMLElement} element The HTML element to update.
* @param {number|null} value The value to set in the element.
*/
const updateTaskCount = (element, value) => {
element.textContent = value === null ? taskQueueSettings.l10n.na : numberFormatter({value: value, locales: taskQueueSettings.l10n.language});
};
/**
* Calculates the time since the given timestamp and returns a formatted string.
* If the timestamp is null or undefined, it returns 'N/A'.
* The returned string is in the format of "X hours, Y minutes" or "X minutes, Y seconds".
*
* @param {string|null} timestamp The timestamp to calculate the time since.
* @returns {string} A formatted string representing the time since the timestamp.
*/
const timeSince = (timestamp) => {
if (!timestamp) {
return taskQueueSettings.l10n.na;
}
const diffSecs = Math.floor((Date.now() - new Date(timestamp)) / 1000);
if (diffSecs >= 3600) {
const hours = Math.floor(diffSecs / 3600);
const minutes = Math.floor((diffSecs % 3600) / 60);
if (minutes > 0) {
const hourText = hours === 1 ? taskQueueSettings.l10n.hour_singular : taskQueueSettings.l10n.hour_plural;
const minuteText = minutes === 1 ? taskQueueSettings.l10n.minute_singular : taskQueueSettings.l10n.minute_plural;
return `${hours} ${hourText}, ${minutes} ${minuteText}`;
}
const hourText = hours === 1 ? taskQueueSettings.l10n.hour_singular : taskQueueSettings.l10n.hour_plural;
return `${hours} ${hourText}`;
}
const units = [
[
60,
taskQueueSettings.l10n.minute_singular,
taskQueueSettings.l10n.minute_plural
],
[
1,
taskQueueSettings.l10n.second_singular,
taskQueueSettings.l10n.second_plural
]
];
for (const [seconds, singular, plural] of units) {
const value = Math.floor(diffSecs / seconds);
if (value > 0) {
return `${value} ${value > 1 ? plural : singular}`;
}
}
return `0 ${taskQueueSettings.l10n.second_plural}`;
};
/**
* Updates the progress bar element and its text content based on the given value and total.
* It calculates the percentage of completion and updates the aria attributes and styles accordingly.
*
* @param {HTMLElement} element The progress bar element to update.
* @param {HTMLElement} textElement The text element to update with the percentage.
* @param {number} value The current value to set in the progress bar.
* @param {number} total The total value for calculating the percentage.
*/
const updateProgressBar = (element, textElement, value, total) => {
const percentage = total ? (value / total) * 100 : 0;
element.setAttribute('aria-valuenow', percentage.toString());
textElement.textContent = `${numberFormatter({value: percentage.toFixed(0), locales: taskQueueSettings.l10n.language})}%`;
element.style.width = `${percentage}%`;
};
// Update task counts
[
[elements.total, tasksTotal],
[elements.running, tasksRunning],
[elements.queued, tasksQueued],
[elements.succeeded, tasksSucceeded],
[elements.retried, tasksRetried],
[elements.failed, tasksFailed]
].forEach(([element, value]) => {
updateTaskCount(element, value);
});
// Update uptime
elements.uptime.textContent = timeSince(earliestTask);
// Update progress bar title
const [
titleTextSucceeded,
titleTextRetried,
titleTextFailed
] = [
[tasksSucceeded, taskQueueSettings.l10n.succeeded],
[tasksRetried, taskQueueSettings.l10n.retried],
[tasksFailed, taskQueueSettings.l10n.failed]
].map(([count, label]) => {
return `${numberFormatter({value: count, locales: taskQueueSettings.l10n.language})} ${label}`;
});
// Set the title attribute for the progress bar
elemProgressBar.setAttribute(
'title',
`${titleTextSucceeded}, ${titleTextRetried}, ${titleTextFailed}`
);
// Update progress bars
[
tasksSucceeded,
tasksRetried,
tasksFailed
].forEach((count, index) => {
const type = ['succeeded', 'retried', 'failed'][index];
updateProgressBar(
progressElements[type].bar,
progressElements[type].text,
count,
tasksTotal
);
});
})
.catch((error) => {
console.error('Error fetching task queue:', error);
// If there is an error fetching the task queue, set all elements to 'ERROR'
[
elements.running,
elements.queued,
elements.succeeded,
elements.retried,
elements.failed
].forEach((elem) => {
elem.textContent = taskQueueSettings.l10n.error;
});
});
};
updateTaskCount();
setInterval(updateTaskCount, 30000);
});

View File

@ -0,0 +1,30 @@
$(document).ready(() => {
'use strict';
const sidebar = document.getElementById('sidebar');
const sidebarKey = `sidebar_${sidebar.id}`;
sidebar.addEventListener('shown.bs.collapse', (event) => {
if (event.target.id === sidebar.id) {
localStorage.removeItem(sidebarKey);
}
});
sidebar.addEventListener('hidden.bs.collapse', (event) => {
if (event.target.id === sidebar.id) {
localStorage.setItem(sidebarKey, 'closed');
}
});
sidebar.classList.toggle('show', localStorage.getItem(sidebarKey) !== 'closed');
const activeChildMenuItem = document.querySelector('ul#sidebar-menu ul.collapse a.active');
if (activeChildMenuItem) {
const activeChildMenuUl = activeChildMenuItem.closest('ul');
activeChildMenuUl.classList.add('show');
document.querySelectorAll(`[data-bs-target^="#${activeChildMenuUl.id}"]`)
.forEach(element => element.setAttribute('aria-expanded', true));
}
});

View File

@ -1,29 +1,32 @@
{% load i18n %} {% load i18n %}
<div id="esi-alert" class="col-12 collapse"> <div id="esi-alert" class="col-12 collapse">
<div class="alert alert-warning"> <div class="alert alert-warning">
<p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p> <p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p>
<hr> <hr>
<pre id="esi-data" class="text-center text-wrap"></pre> <pre id="esi-data" class="text-center text-wrap"></pre>
</div> </div>
</div>
<script> <script>
const elemCard = document.getElementById('esi-alert'); $(document).ready(() => {
const elemMessage = document.getElementById('esi-data'); const elements = {
const elemCode = document.getElementById('esi-code'); card: document.getElementById('esi-alert'),
message: document.getElementById('esi-data'),
code: document.getElementById('esi-code')
};
fetchGet({url: '{% url "authentication:esi_check" %}'}) fetchGet({url: '{% url "authentication:esi_check" %}'})
.then((data) => { .then(({status, data}) => {
console.log('ESI Check: ', JSON.stringify(data, null, 2)); console.log('ESI Check:', JSON.stringify({status, data}, null, 2));
if (data.status !== 200) { if (status !== 200) {
elemCode.textContent = data.status; elements.code.textContent = status;
elemMessage.textContent = data.data.error; elements.message.textContent = data.error;
new bootstrap.Collapse(elemCard, {toggle: true}); new bootstrap.Collapse(elements.card, {toggle: true});
} }
}) })
.catch((error) => { .catch((error) => console.error('Error fetching ESI check:', error));
console.error('Error fetching ESI check:', error);
}); });
</script> </script>
</div>

View File

@ -161,206 +161,24 @@ the escapejs filter without having to redefine them later.
</div> </div>
<script> <script>
const elements = { const taskQueueSettings = {
total: document.getElementById('total-task-count'), url: '{% url "authentication:task_counts" %}',
uptime: document.getElementById('celery-uptime'), l10n: {
running: document.getElementById('running-task-count'), language: '{{ LANGUAGE_CODE }}',
queued: document.getElementById('queued-tasks-count'), second_singular: '{{ l10nSecondSingular|escapejs }}',
succeeded: document.getElementById('succeeded-tasks-count'), second_plural: '{{ l10nSecondPlural|escapejs }}',
retried: document.getElementById('retried-tasks-count'), minute_singular: '{{ l10nMinuteSingular|escapejs }}',
failed: document.getElementById('failed-tasks-count') minute_plural: '{{ l10nMinutePlural|escapejs }}',
}; hour_singular: '{{ l10nHourSingular|escapejs }}',
hour_plural: '{{ l10nHourPlural|escapejs }}',
/** na: '{{ l10nNA|escapejs }}',
* Fetches the task queue status and updates the UI elements accordingly. error: '{{ l10nError|escapejs }}',
* It retrieves the total number of tasks, running tasks, queued tasks, running: '{{ l10nRunning|escapejs }}',
* succeeded tasks, retried tasks, and failed tasks, and updates the queued: '{{ l10nQueued|escapejs }}',
* corresponding HTML elements with the fetched data. succeeded: '{{ l10nSucceeded|escapejs }}',
* It also updates the progress bars for succeeded, retried, and failed tasks. retried: '{{ l10nRetried|escapejs }}',
* The function is called immediately and then every 30 seconds to keep the failed: '{{ l10nFailed|escapejs }}'
* task queue status up to date.
*/
const updateTaskCount = () => {
fetchGet({url: '{% url "authentication:task_counts" %}'})
.then((data) => {
const numberL10nFormat = new Intl.NumberFormat('{{ LANGUAGE_CODE }}');
const elemProgressBar = document.getElementById('celery-tasks-progress-bar');
const progressElements = {
succeeded: {
bar: document.getElementById('celery-progress-bar-succeeded'),
text: document.getElementById('celery-progress-bar-succeeded-progress')
},
retried: {
bar: document.getElementById('celery-progress-bar-retried'),
text: document.getElementById('celery-progress-bar-retried-progress')
},
failed: {
bar: document.getElementById('celery-progress-bar-failed'),
text: document.getElementById('celery-progress-bar-failed-progress')
} }
}; };
// Assign progress data from the fetched data to variables
const {
earliest_task: earliestTask,
tasks_total: tasksTotal,
tasks_running: tasksRunning,
tasks_queued: tasksQueued,
tasks_succeeded: tasksSucceeded,
tasks_retried: tasksRetried,
tasks_failed: tasksFailed
} = data;
/**
* Updates the text content of the specified HTML element with the given value.
* If the value is null, it sets the text to 'N/A'.
* Otherwise, it formats the number using the locale-specific format.
*
* @param {HTMLElement} element The HTML element to update.
* @param {number|null} value The value to set in the element.
*/
const updateTaskCount = (element, value) => {
element.textContent = value == null ? '{{ l10nNA|escapejs }}' : numberL10nFormat.format(value);
};
/**
* Calculates the time since the given timestamp and returns a formatted string.
* If the timestamp is null or undefined, it returns 'N/A'.
* The returned string is in the format of "X hours, Y minutes" or "X minutes, Y seconds".
*
* @param {string|null} timestamp The timestamp to calculate the time since.
* @returns {string} A formatted string representing the time since the timestamp.
*/
const timeSince = (timestamp) => {
if (!timestamp) {
return '{{ l10nNA|escapejs }}';
}
const diffSecs = Math.floor((Date.now() - new Date(timestamp)) / 1000);
if (diffSecs >= 3600) {
const hours = Math.floor(diffSecs / 3600);
const minutes = Math.floor((diffSecs % 3600) / 60);
if (minutes > 0) {
const hourText = hours === 1 ? '{{ l10nHourSingular|escapejs }}' : '{{ l10nHourPlural|escapejs }}';
const minuteText = minutes === 1 ? '{{ l10nMinuteSingular|escapejs }}' : '{{ l10nMinutePlural|escapejs }}';
return `${hours} ${hourText}, ${minutes} ${minuteText}`;
}
const hourText = hours === 1 ? '{{ l10nHourSingular|escapejs }}' : '{{ l10nHourPlural|escapejs }}';
return `${hours} ${hourText}`;
}
const units = [
[
60,
'{{ l10nMinuteSingular|escapejs }}',
'{{ l10nMinutePlural|escapejs }}'
],
[
1,
'{{ l10nSecondSingular|escapejs }}',
'{{ l10nSecondPlural|escapejs }}'
]
];
for (const [seconds, singular, plural] of units) {
const value = Math.floor(diffSecs / seconds);
if (value > 0) {
return `${value} ${value > 1 ? plural : singular}`;
}
}
return '0 {{ l10nSecondPlural|escapejs }}';
};
/**
* Updates the progress bar element and its text content based on the given value and total.
* It calculates the percentage of completion and updates the aria attributes and styles accordingly.
*
* @param {HTMLElement} element The progress bar element to update.
* @param {HTMLElement} textElement The text element to update with the percentage.
* @param {number} value The current value to set in the progress bar.
* @param {number} total The total value for calculating the percentage.
*/
const updateProgressBar = (element, textElement, value, total) => {
const percentage = total ? (value / total) * 100 : 0;
element.setAttribute('aria-valuenow', percentage.toString());
textElement.textContent = `${numberL10nFormat.format(percentage.toFixed(0))}%`;
element.style.width = `${percentage}%`;
};
// Update task counts
[
[elements.total, tasksTotal],
[elements.running, tasksRunning],
[elements.queued, tasksQueued],
[elements.succeeded, tasksSucceeded],
[elements.retried, tasksRetried],
[elements.failed, tasksFailed]
].forEach(([element, value]) => {
updateTaskCount(element, value);
});
// Update uptime
elements.uptime.textContent = timeSince(earliestTask);
// Update progress bar title
const [
titleTextSucceeded,
titleTextRetried,
titleTextFailed
] = [
[tasksSucceeded, '{{ l10nSucceeded|escapejs }}'],
[tasksRetried, '{{ l10nRetried|escapejs }}'],
[tasksFailed, '{{ l10nFailed|escapejs }}']
].map(([count, label]) => {
return `${numberL10nFormat.format(count)} ${label}`;
});
// Set the title attribute for the progress bar
elemProgressBar.setAttribute(
'title',
`${titleTextSucceeded}, ${titleTextRetried}, ${titleTextFailed}`
);
// Update progress bars
[
tasksSucceeded,
tasksRetried,
tasksFailed
].forEach((count, index) => {
const type = ['succeeded', 'retried', 'failed'][index];
updateProgressBar(
progressElements[type].bar,
progressElements[type].text,
count,
tasksTotal
);
});
})
.catch((error) => {
console.error('Error fetching task queue:', error);
// If there is an error fetching the task queue, set all elements to 'ERROR'
[
elements.running,
elements.queued,
elements.succeeded,
elements.retried,
elements.failed
].forEach((elem) => {
elem.textContent = '{{ l10nError|escapejs }}';
});
});
};
updateTaskCount();
setInterval(updateTaskCount, 30000);
</script> </script>
{% include "bundles/auth-dashboard-task-queue-js.html" %}

View File

@ -102,44 +102,7 @@
</main> </main>
<!-- End Body --> <!-- End Body -->
<script> {% include "bundles/auth-sidebar-collapse-js.html" %}
(() => {
// TODO Move to own JS file
const sidebar = document.getElementById('sidebar');
const sidebarKey = `sidebar_${sidebar.id}`;
sidebar.addEventListener('shown.bs.collapse', (event) => {
if (event.target.id === sidebar.id) {
localStorage.removeItem(sidebarKey);
}
});
sidebar.addEventListener('hidden.bs.collapse', (event) => {
if (event.target.id === sidebar.id) {
localStorage.setItem(sidebarKey, 'closed');
}
});
if (localStorage.getItem(sidebarKey) === 'closed') {
sidebar.classList.remove('show');
} else {
sidebar.classList.add('show');
}
const activeChildMenuItem = document.querySelector('#sidebar-menu li ul li a.active');
if (activeChildMenuItem) {
const activeChildMenuUl = activeChildMenuItem.parentElement.parentElement;
const elementsToToggle = document.querySelectorAll(`[data-bs-target^="#${activeChildMenuUl.id}"]`);
activeChildMenuUl.classList.add('show');
elementsToToggle.forEach((element) => {
element.setAttribute('aria-expanded', true);
});
}
})();
</script>
{% theme_js %} {% theme_js %}

View File

@ -0,0 +1,3 @@
{% load sri %}
{% sri_static 'allianceauth/js/dashboard-update-task-queue.js' %}

View File

@ -0,0 +1,3 @@
{% load sri %}
{% sri_static 'allianceauth/js/sidebar-collapse.js' %}

View File

@ -96,6 +96,7 @@
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %} {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => {
const timers = [ const timers = [
{% for timer in timers %} {% for timer in timers %}
{ {
@ -175,7 +176,6 @@
// Start timed updates // Start timed updates
setInterval(timedUpdate, 1000); setInterval(timedUpdate, 1000);
$(document).ready(() => {
const dtOptions = { const dtOptions = {
language: {url: '{{ DT_LANG_PATH }}'}, language: {url: '{{ DT_LANG_PATH }}'},
order: [ order: [
@ -185,7 +185,10 @@
{% if perms.auth.timer_management %} {% if perms.auth.timer_management %}
dtOptions['columnDefs'] = [ dtOptions['columnDefs'] = [
{ "orderable": false, "targets": 7 } {
"orderable": false,
"targets": 7
}
]; ];
{% endif %} {% endif %}

View File

@ -26,7 +26,7 @@ app.conf.task_default_priority = 5 # anything called with the task.delay() will
app.conf.worker_prefetch_multiplier = 1 # only prefetch single tasks at a time on the workers so that prio tasks happen app.conf.worker_prefetch_multiplier = 1 # only prefetch single tasks at a time on the workers so that prio tasks happen
app.conf.ONCE = { app.conf.ONCE = {
'backend': 'allianceauth.services.tasks.DjangoBackend', 'backend': 'allianceauth.services.celery_once.backends.DjangoBackend',
'settings': {} 'settings': {}
} }