mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-12 18:16:24 +01:00
Compare commits
149 Commits
757c6fa491
...
v5.0.0a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
826198e5a7 | ||
|
|
cc8f53af12 | ||
|
|
f134b17c66 | ||
|
|
4036b0272c | ||
|
|
33b3c5b36e | ||
|
|
9547826272 | ||
|
|
15fc38ccfd | ||
|
|
055077fa77 | ||
|
|
f342ccbc6a | ||
|
|
37ffd0a1ac | ||
|
|
a1f705381e | ||
|
|
c0970ad4fa | ||
|
|
3818d0c6d1 | ||
|
|
95411c79cb | ||
|
|
eeccbbacfc | ||
|
|
f6c4180502 | ||
|
|
f4d3d6c0b1 | ||
|
|
e9d2d11297 | ||
|
|
96204b29e8 | ||
|
|
47842c1243 | ||
|
|
9b494106bc | ||
|
|
d51e730a7f | ||
|
|
363909c0c4 | ||
|
|
82273f68fe | ||
|
|
12fa38b446 | ||
|
|
c26af593ff | ||
|
|
8e9a53c494 | ||
|
|
5559ce5fbb | ||
|
|
faa529a55b | ||
|
|
4ccfe20c14 | ||
|
|
960c9625fe | ||
|
|
7b92d103d6 | ||
|
|
c1e2449084 | ||
|
|
3acb651650 | ||
|
|
2de57b334b | ||
|
|
0498f5bb1b | ||
|
|
929485a8f9 | ||
|
|
28cb62f373 | ||
|
|
02214b74d0 | ||
|
|
f497c18e5b | ||
|
|
cb57d922e6 | ||
|
|
805d138b09 | ||
|
|
09a583fb1d | ||
|
|
146c4c8d94 | ||
|
|
c2ae680f72 | ||
|
|
b5ad1c8a1a | ||
|
|
8be2760fc4 | ||
|
|
f047943eb7 | ||
|
|
43906f41b3 | ||
|
|
a18ec98877 | ||
|
|
14163d2c0c | ||
|
|
81d9c41cf6 | ||
|
|
58f5a5b41d | ||
|
|
6363bb706a | ||
|
|
baf3be4cb2 | ||
|
|
e69444fe79 | ||
|
|
7483fcb876 | ||
|
|
a57d55504d | ||
|
|
affb30e9f4 | ||
|
|
588cb1b7ca | ||
|
|
a609faa91b | ||
|
|
856e939c21 | ||
|
|
1b6cf98885 | ||
|
|
92c2af9975 | ||
|
|
5ef70bb031 | ||
|
|
60998bffc2 | ||
|
|
a5971314f5 | ||
|
|
a03c766840 | ||
|
|
ad47ff2c54 | ||
|
|
3efdb8f12b | ||
|
|
823fc82d19 | ||
|
|
a93e510895 | ||
|
|
d99f5858d8 | ||
|
|
4578ecf21d | ||
|
|
b737504d52 | ||
|
|
c6b6443901 | ||
|
|
f51523dc07 | ||
|
|
bd4dd60c98 | ||
|
|
a4ea48e14e | ||
|
|
646d3f5408 | ||
|
|
0f057ffa84 | ||
|
|
d10562e9fc | ||
|
|
168b023a72 | ||
|
|
9df76443b1 | ||
|
|
5c07f75eb5 | ||
|
|
d61a49f2d9 | ||
|
|
7033406ba6 | ||
|
|
6b395ca1d4 | ||
|
|
795a7e006f | ||
|
|
2a894cd62c | ||
|
|
9ada26e849 | ||
|
|
7120b3956c | ||
|
|
4da67cfaf6 | ||
|
|
0a940810bd | ||
|
|
a868438492 | ||
|
|
dc1ed8c570 | ||
|
|
a3c6d5345b | ||
|
|
8489f204dd | ||
|
|
5e836c4285 | ||
|
|
dc0c1a2818 | ||
|
|
eaba01ad97 | ||
|
|
f4c024d199 | ||
|
|
8f4daea14f | ||
|
|
b95f393a4c | ||
|
|
1478588016 | ||
|
|
a16eb4b7f7 | ||
|
|
292fb7b29d | ||
|
|
c6890dd2c6 | ||
|
|
702564d15e | ||
|
|
cef2e86ea1 | ||
|
|
50681b023b | ||
|
|
2822775fb8 | ||
|
|
ef7c8be7b5 | ||
|
|
d639617eba | ||
|
|
2125192f72 | ||
|
|
8d63801b00 | ||
|
|
e053fb7d96 | ||
|
|
ae7ed5c297 | ||
|
|
d624ba4427 | ||
|
|
164cd4fbb2 | ||
|
|
94b52c850e | ||
|
|
4d19ceb388 | ||
|
|
9a64728311 | ||
|
|
dcc0c78daf | ||
|
|
5507c30af4 | ||
|
|
66b97835d4 | ||
|
|
fd66a7cb20 | ||
|
|
c0f0f8db73 | ||
|
|
4210b2eabc | ||
|
|
225e68647e | ||
|
|
7e2f864ebf | ||
|
|
f2384ba45b | ||
|
|
c5918b9b3c | ||
|
|
ffedc4103d | ||
|
|
0467b23a1a | ||
|
|
cda5ce739f | ||
|
|
e5c8426ea3 | ||
|
|
b2bd489ddc | ||
|
|
6397cf358a | ||
|
|
90fcc4a811 | ||
|
|
ec7472fe22 | ||
|
|
5148b1914d | ||
|
|
2e0716f5ae | ||
|
|
1fb091acb2 | ||
|
|
07c62ed32a | ||
|
|
660fe79d08 | ||
|
|
1dea92ed76 | ||
|
|
3f54d49d8b | ||
|
|
77da6928b2 |
@@ -98,7 +98,6 @@ test-3.13-core:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
test-3.10-all:
|
||||
<<: *only-default
|
||||
@@ -148,7 +147,6 @@ test-3.13-all:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
build-test:
|
||||
stage: test
|
||||
|
||||
@@ -3,22 +3,48 @@
|
||||
# Update this file:
|
||||
# pre-commit autoupdate
|
||||
|
||||
# Set the default language versions for the hooks
|
||||
default_language_version:
|
||||
python: python3 # Force all Python hooks to use Python 3
|
||||
node: 22.12.0 # Force all Node hooks to use Node 22.12.0
|
||||
|
||||
# Globally exclude files
|
||||
# https://pre-commit.com/#top_level-exclude
|
||||
exclude: |
|
||||
(?x)(
|
||||
LICENSE|
|
||||
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
|
||||
\.min\.css|
|
||||
\.min\.js|
|
||||
\.po|
|
||||
\.mo|
|
||||
swagger\.json|
|
||||
static/(.*)/libs/|
|
||||
telnetlib\.py
|
||||
)
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.4
|
||||
rev: v0.9.9
|
||||
hooks:
|
||||
# Run the linter, and only the linter
|
||||
- id: ruff
|
||||
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.21.0
|
||||
rev: 1.23.1
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [--target-version=4.2]
|
||||
args: [--target-version=5.1]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade # Ruff doesnt get everything.
|
||||
rev: v3.19.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
|
||||
# Formatting
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
# Identify invalid files
|
||||
- id: check-ast
|
||||
@@ -33,9 +59,9 @@ repos:
|
||||
- id: detect-private-key
|
||||
- id: check-case-conflict
|
||||
# Python checks
|
||||
# - id: check-docstring-first
|
||||
# - id: check-docstring-first
|
||||
- id: debug-statements
|
||||
# - id: requirements-txt-fixer
|
||||
# - id: requirements-txt-fixer
|
||||
- id: fix-encoding-pragma
|
||||
args: [--remove]
|
||||
- id: fix-byte-order-marker
|
||||
@@ -44,45 +70,22 @@ repos:
|
||||
args: [--fix=lf]
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
exclude: |
|
||||
(?x)(
|
||||
\.min\.css|
|
||||
\.min\.js|
|
||||
\.po|
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
- id: check-executables-have-shebangs
|
||||
- id: end-of-file-fixer
|
||||
exclude: |
|
||||
(?x)(
|
||||
\.min\.css|
|
||||
\.min\.js|
|
||||
\.po|
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 3.0.3
|
||||
rev: 3.2.0
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
exclude: |
|
||||
(?x)(
|
||||
LICENSE|
|
||||
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
|
||||
\.po|
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.41.0
|
||||
rev: v0.44.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
language: node
|
||||
args:
|
||||
- --disable=MD013
|
||||
# Infrastructure
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 2.2.3
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
name: pyproject.toml formatter
|
||||
@@ -90,9 +93,9 @@ repos:
|
||||
args:
|
||||
- --indent=4
|
||||
additional_dependencies:
|
||||
- tox==4.18.1 # https://github.com/tox-dev/tox/releases/latest
|
||||
- tox==4.24.1 # https://github.com/tox-dev/tox/releases/latest
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.19
|
||||
rev: v0.23
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
name: Validate pyproject.toml
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[](https://pypi.org/project/allianceauth/)
|
||||
[](https://pypi.org/project/allianceauth/)
|
||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||
[](http://allianceauth.readthedocs.io/?badge=latest)
|
||||
[](https://allianceauth.readthedocs.io/?badge=latest)
|
||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||
[](https://discord.gg/fjnHAmk)
|
||||
|
||||
@@ -14,7 +14,7 @@ An auth system for EVE Online to help in-game organizations manage online servic
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Documentation](http://allianceauth.rtfd.io)
|
||||
- [Documentation](https://allianceauth.rtfd.io)
|
||||
- [Support](#support)
|
||||
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
|
||||
- [Developer Team](#development-team)
|
||||
@@ -38,7 +38,7 @@ Main features:
|
||||
|
||||
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr:, Russian :flag_ru:, Italian :flag_it:, French :flag_fr:, Japanese :flag_jp: and Ukrainian :flag_ua: Localization
|
||||
|
||||
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
|
||||
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](https://allianceauth.rtfd.io).
|
||||
|
||||
## Screenshot
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ manage online service access.
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
__version__ = '4.3.1'
|
||||
__version__ = '5.0.0a1'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = f'{__title__} v{__version__}'
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from solo.admin import SingletonModelAdmin
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import AnalyticsIdentifier, AnalyticsTokens
|
||||
|
||||
|
||||
@admin.register(AnalyticsIdentifier)
|
||||
class AnalyticsIdentifierAdmin(admin.ModelAdmin):
|
||||
class AnalyticsIdentifierAdmin(SingletonModelAdmin):
|
||||
search_fields = ['identifier', ]
|
||||
list_display = ('identifier',)
|
||||
list_display = ['identifier', ]
|
||||
|
||||
|
||||
@admin.register(AnalyticsTokens)
|
||||
class AnalyticsTokensAdmin(admin.ModelAdmin):
|
||||
search_fields = ['name', ]
|
||||
list_display = ('name', 'type',)
|
||||
list_display = ['name', 'type', ]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.16 on 2024-12-11 02:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('analytics', '0009_remove_analyticstokens_ignore_paths_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='analyticsidentifier',
|
||||
options={'verbose_name': 'Analytics Identifier'},
|
||||
),
|
||||
]
|
||||
@@ -1,27 +1,21 @@
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from solo.models import SingletonModel
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AnalyticsIdentifier(models.Model):
|
||||
class AnalyticsIdentifier(SingletonModel):
|
||||
|
||||
identifier = models.UUIDField(
|
||||
default=uuid4,
|
||||
editable=False)
|
||||
identifier = models.UUIDField(default=uuid4, editable=False)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.identifier}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk and AnalyticsIdentifier.objects.exists():
|
||||
# Force a single object
|
||||
raise ValidationError('There is can be only one \
|
||||
AnalyticsIdentifier instance')
|
||||
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
|
||||
return super().save(*args, **kwargs)
|
||||
def __str__(self) -> Literal['Analytics Identifier']:
|
||||
return "Analytics Identifier"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Analytics Identifier"
|
||||
|
||||
class AnalyticsTokens(models.Model):
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.conf import settings
|
||||
from allianceauth import __version__
|
||||
|
||||
from .models import AnalyticsIdentifier, AnalyticsTokens
|
||||
from .utils import install_stat_addons, install_stat_tokens, install_stat_users
|
||||
from .utils import existence_baremetal_or_docker, install_stat_addons, install_stat_tokens, install_stat_users
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,8 +67,8 @@ def analytics_event(namespace: str,
|
||||
value=value).apply_async(priority=9)
|
||||
|
||||
|
||||
@shared_task()
|
||||
def analytics_daily_stats():
|
||||
@shared_task
|
||||
def analytics_daily_stats() -> None:
|
||||
"""Celery Task: Do not call directly
|
||||
|
||||
Gathers a series of daily statistics
|
||||
@@ -77,6 +77,7 @@ def analytics_daily_stats():
|
||||
users = install_stat_users()
|
||||
tokens = install_stat_tokens()
|
||||
addons = install_stat_addons()
|
||||
existence_type = existence_baremetal_or_docker()
|
||||
logger.debug("Running Daily Analytics Upload")
|
||||
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
@@ -84,6 +85,11 @@ def analytics_daily_stats():
|
||||
label='existence',
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_install_stats',
|
||||
label=existence_type,
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_install_stats',
|
||||
label='users',
|
||||
@@ -99,7 +105,6 @@ def analytics_daily_stats():
|
||||
label='addons',
|
||||
value=addons,
|
||||
event_type='Stats')
|
||||
|
||||
for appconfig in apps.get_app_configs():
|
||||
if appconfig.label in [
|
||||
"django_celery_beat",
|
||||
@@ -135,7 +140,7 @@ def analytics_daily_stats():
|
||||
event_type='Stats')
|
||||
|
||||
|
||||
@shared_task()
|
||||
@shared_task
|
||||
def send_ga_tracking_celery_event(
|
||||
measurement_id: str,
|
||||
secret: str,
|
||||
@@ -165,7 +170,7 @@ def send_ga_tracking_celery_event(
|
||||
}
|
||||
|
||||
payload = {
|
||||
'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex,
|
||||
'client_id': AnalyticsIdentifier.get_solo().identifier.hex,
|
||||
"user_properties": {
|
||||
"allianceauth_version": {
|
||||
"value": __version__
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from allianceauth.analytics.models import AnalyticsIdentifier
|
||||
@@ -13,14 +12,4 @@ uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
|
||||
class TestAnalyticsIdentifier(TestCase):
|
||||
|
||||
def test_identifier_random(self):
|
||||
self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4)
|
||||
|
||||
def test_identifier_singular(self):
|
||||
AnalyticsIdentifier.objects.all().delete()
|
||||
AnalyticsIdentifier.objects.create(identifier=uuid_1)
|
||||
# Yeah i have multiple asserts here, they all do the same thing
|
||||
with self.assertRaises(ValidationError):
|
||||
AnalyticsIdentifier.objects.create(identifier=uuid_2)
|
||||
self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
|
||||
self.assertEqual(AnalyticsIdentifier.objects.get(
|
||||
pk=1).identifier, UUID(uuid_1))
|
||||
self.assertNotEqual(AnalyticsIdentifier.get_solo(), uuid4)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
from esi.models import Token
|
||||
@@ -36,3 +38,16 @@ def install_stat_addons() -> int:
|
||||
The Number of Installed Apps"""
|
||||
addons = len(list(apps.get_app_configs()))
|
||||
return addons
|
||||
|
||||
|
||||
def existence_baremetal_or_docker() -> str:
|
||||
"""Checks the Installation Type of an install
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
existence_baremetal or existence_docker"""
|
||||
docker_tag = os.getenv('AA_DOCKER_TAG')
|
||||
if docker_tag:
|
||||
return "existence_docker"
|
||||
return "existence_baremetal"
|
||||
|
||||
@@ -126,7 +126,7 @@ class UserProfile(models.Model):
|
||||
self.save(update_fields=['state'])
|
||||
notify(
|
||||
self.user,
|
||||
_('State changed to: {}'.format(state)),
|
||||
_(f'State changed to: {state}'),
|
||||
_('Your user\'s state is now: %(state)s')
|
||||
% ({'state': state}),
|
||||
'info'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
@@ -13,7 +14,7 @@
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="mb-3">
|
||||
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
|
||||
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://developers.eveonline.com/authorized-apps where possible."|urlize %}
|
||||
</p>
|
||||
|
||||
<table class="table w-100" id="table_tokens">
|
||||
@@ -50,20 +51,23 @@
|
||||
{% block extra_javascript %}
|
||||
{% include "bundles/datatables-js-bs5.html" %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
let grp = 2;
|
||||
|
||||
const table = $('#table_tokens').DataTable({
|
||||
$('#table_tokens').DataTable({
|
||||
"language": {"url": '{{ DT_LANG_PATH }}'},
|
||||
'columnDefs': [{orderable: false, targets: [0, 1]}, {
|
||||
'visible': false,
|
||||
'targets': grp
|
||||
}],
|
||||
'order': [[grp, 'asc']],
|
||||
'drawCallback': function (settings) {
|
||||
var api = this.api();
|
||||
var rows = api.rows({page: 'current'}).nodes();
|
||||
var last = null;
|
||||
const api = this.api();
|
||||
const rows = api.rows({page: 'current'}).nodes();
|
||||
let last = null;
|
||||
api.column(grp, {page: 'current'})
|
||||
.data()
|
||||
.each((group, i) => {
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<select class="form-select" onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
|
||||
{% for language in languages %}
|
||||
<option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
{{ language.name_local|capfirst }} ({{ language.code }})
|
||||
{% for lang_code, lang_name in LANGUAGES %}
|
||||
<option lang="{{ lang_code }}" value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
||||
{{ lang_code|language_name_local|capfirst }} ({{ lang_code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
<a class="text-reset" href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
|
||||
<a class="text-reset" href="https://developers.eveonline.com/authorized-apps" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Manage ESI Applications" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ urlpatterns = [
|
||||
name='token_refresh'
|
||||
),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
|
||||
path('task-counts/', views.task_counts, name='task_counts'),
|
||||
path('esi-check/', views.esi_check, name='esi_check'),
|
||||
]
|
||||
|
||||
@@ -175,9 +175,9 @@ def add_character(request, token):
|
||||
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
||||
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
||||
messages.success(request, _(
|
||||
'Added {name} to your account.'.format(name=token.character_name)))
|
||||
f'Added {token.character_name} to your account.'))
|
||||
else:
|
||||
messages.error(request, _('Failed to add {name} to your account: they already have an account.'.format(name=token.character_name)))
|
||||
messages.error(request, _(f'Failed to add {token.character_name} to your account: they already have an account.'))
|
||||
return redirect('authentication:dashboard')
|
||||
|
||||
|
||||
@@ -392,12 +392,3 @@ def esi_check(request) -> JsonResponse:
|
||||
"data": check_for_override_esi_error_message(_r)
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard_bs3(request):
|
||||
"""Render dashboard view with BS3 theme.
|
||||
|
||||
This is an internal view used for testing BS3 backward compatibility in AA4 only.
|
||||
"""
|
||||
return render(request, 'authentication/dashboard_bs3.html')
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import datetime
|
||||
"""
|
||||
Django system checks for Alliance Auth
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from sqlite3.dbapi2 import sqlite_version_info
|
||||
|
||||
from celery import current_app
|
||||
@@ -7,7 +11,6 @@ from packaging.version import InvalidVersion, Version as Pep440Version
|
||||
from django import db
|
||||
from django.conf import settings
|
||||
from django.core.checks import CheckMessage, Error, Warning, register
|
||||
from django.utils import timezone
|
||||
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
|
||||
@@ -19,141 +22,415 @@ B = Configuration
|
||||
|
||||
@register()
|
||||
def django_settings(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
errors: list[CheckMessage] = []
|
||||
if hasattr(settings, "SITE_URL"):
|
||||
if settings.SITE_URL[-1] == "/":
|
||||
errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005"))
|
||||
else:
|
||||
errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006"))
|
||||
"""
|
||||
Check that Django settings are correctly configured
|
||||
|
||||
if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"):
|
||||
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
|
||||
errors.append(Warning("'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", hint="", id="allianceauth.checks.B007"))
|
||||
:param app_configs:
|
||||
:type app_configs:
|
||||
:param kwargs:
|
||||
:type kwargs:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
# Check for SITE_URL
|
||||
if hasattr(settings, "SITE_URL"):
|
||||
# Check if SITE_URL is empty
|
||||
if settings.SITE_URL == "":
|
||||
errors.append(
|
||||
Error(
|
||||
msg="'SITE_URL' is empty.",
|
||||
hint="Make sure to set 'SITE_URL' to the URL of your Auth instance. (Without trailing slash)",
|
||||
id="allianceauth.checks.B011",
|
||||
)
|
||||
)
|
||||
# Check if SITE_URL has a trailing slash
|
||||
elif settings.SITE_URL[-1] == "/":
|
||||
errors.append(
|
||||
Warning(
|
||||
msg="'SITE_URL' has a trailing slash. This may lead to incorrect links being generated by Auth.",
|
||||
hint="",
|
||||
id="allianceauth.checks.B005",
|
||||
)
|
||||
)
|
||||
# SITE_URL not found
|
||||
else:
|
||||
errors.append(Error("No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", hint="", id="allianceauth.checks.B008"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg="No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.",
|
||||
hint="",
|
||||
id="allianceauth.checks.B006",
|
||||
)
|
||||
)
|
||||
|
||||
# Check for CSRF_TRUSTED_ORIGINS
|
||||
if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"):
|
||||
# Check if SITE_URL is not in CSRF_TRUSTED_ORIGINS
|
||||
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
|
||||
errors.append(
|
||||
Warning(
|
||||
msg="'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.",
|
||||
hint="",
|
||||
id="allianceauth.checks.B007",
|
||||
)
|
||||
)
|
||||
# CSRF_TRUSTED_ORIGINS not found
|
||||
else:
|
||||
errors.append(
|
||||
Error(
|
||||
msg="No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified",
|
||||
hint="",
|
||||
id="allianceauth.checks.B008",
|
||||
)
|
||||
)
|
||||
|
||||
# Check for ESI_USER_CONTACT_EMAIL
|
||||
if hasattr(settings, "ESI_USER_CONTACT_EMAIL"):
|
||||
# Check if ESI_USER_CONTACT_EMAIL is empty
|
||||
if settings.ESI_USER_CONTACT_EMAIL == "":
|
||||
errors.append(
|
||||
Error(
|
||||
msg="'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.",
|
||||
hint="",
|
||||
id="allianceauth.checks.B009",
|
||||
)
|
||||
)
|
||||
# ESI_USER_CONTACT_EMAIL not found
|
||||
else:
|
||||
errors.append(
|
||||
Error(
|
||||
msg="No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.",
|
||||
hint="",
|
||||
id="allianceauth.checks.B010",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
"""
|
||||
Check that Redis is a supported version
|
||||
|
||||
:param app_configs:
|
||||
:type app_configs:
|
||||
:param kwargs:
|
||||
:type kwargs:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
allianceauth_redis_install_link = "https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools"
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
try:
|
||||
redis_version = Pep440Version(get_redis_client().info()['redis_version'])
|
||||
redis_version = Pep440Version(get_redis_client().info()["redis_version"])
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm Redis Version"))
|
||||
|
||||
return errors
|
||||
|
||||
if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001"))
|
||||
if (
|
||||
redis_version.major == 7
|
||||
and redis_version.minor == 2
|
||||
and datetime.now(timezone.utc)
|
||||
> datetime(year=2025, month=8, day=31, tzinfo=timezone.utc)
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
|
||||
hint=allianceauth_redis_install_link,
|
||||
id="allianceauth.checks.A001",
|
||||
)
|
||||
)
|
||||
elif redis_version.major == 7 and redis_version.minor == 0:
|
||||
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A002"))
|
||||
errors.append(
|
||||
Warning(
|
||||
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
|
||||
hint=allianceauth_redis_install_link,
|
||||
id="allianceauth.checks.A002",
|
||||
)
|
||||
)
|
||||
elif redis_version.major == 6 and redis_version.minor == 2:
|
||||
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A018"))
|
||||
errors.append(
|
||||
Warning(
|
||||
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
|
||||
hint=allianceauth_redis_install_link,
|
||||
id="allianceauth.checks.A018",
|
||||
)
|
||||
)
|
||||
elif redis_version.major in [6, 5]:
|
||||
errors.append(Error(f"Redis {redis_version.public} EOL", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A003"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"Redis {redis_version.public} EOL",
|
||||
hint=allianceauth_redis_install_link,
|
||||
id="allianceauth.checks.A003",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
"""
|
||||
Check that MySQL is a supported version
|
||||
|
||||
:param app_configs:
|
||||
:type app_configs:
|
||||
:param kwargs:
|
||||
:type kwargs:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
mysql_quick_guide_link = "https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/"
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "mysql":
|
||||
try:
|
||||
mysql_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
|
||||
mysql_version = Pep440Version(
|
||||
".".join(str(i) for i in connection.mysql_version)
|
||||
)
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm MySQL Version"))
|
||||
|
||||
return errors
|
||||
|
||||
# MySQL 8
|
||||
if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 3:
|
||||
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 2:
|
||||
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A006"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 1:
|
||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008"))
|
||||
elif mysql_version.major < 8: # This will also catch Mariadb 5.x
|
||||
errors.append(Error(f"MySQL or MariaDB {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A009"))
|
||||
# MySQL 8
|
||||
if mysql_version.major == 8:
|
||||
if mysql_version.minor == 4 and datetime.now(timezone.utc) > datetime(
|
||||
year=2032, month=4, day=30, tzinfo=timezone.utc
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MySQL {mysql_version.public} EOL",
|
||||
hint=mysql_quick_guide_link,
|
||||
id="allianceauth.checks.A004",
|
||||
)
|
||||
)
|
||||
# Demote versions down here once EOL
|
||||
elif mysql_version.minor in [1, 2, 3]:
|
||||
errors.append(
|
||||
Warning(
|
||||
msg=f"MySQL {mysql_version.public} Non LTS",
|
||||
hint=mysql_quick_guide_link,
|
||||
id="allianceauth.checks.A005",
|
||||
)
|
||||
)
|
||||
elif mysql_version.minor == 0 and datetime.now(timezone.utc) > datetime(
|
||||
year=2026, month=4, day=30, tzinfo=timezone.utc
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MySQL {mysql_version.public} EOL",
|
||||
hint=mysql_quick_guide_link,
|
||||
id="allianceauth.checks.A008",
|
||||
)
|
||||
)
|
||||
|
||||
# MySQL below 8
|
||||
# This will also catch Mariadb 5.x
|
||||
elif mysql_version.major < 8:
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MySQL or MariaDB {mysql_version.public} EOL",
|
||||
hint=mysql_quick_guide_link,
|
||||
id="allianceauth.checks.A009",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: # noqa: C901
|
||||
"""
|
||||
Check that MariaDB is a supported version
|
||||
"""
|
||||
|
||||
mariadb_download_link = "https://mariadb.org/download/?t=repo-config"
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB
|
||||
# TODO: Find a way to determine MySQL vs. MariaDB
|
||||
if connection.vendor == "mysql":
|
||||
try:
|
||||
mariadb_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
|
||||
mariadb_version = Pep440Version(
|
||||
".".join(str(i) for i in connection.mysql_version)
|
||||
)
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm MariaDB Version"))
|
||||
|
||||
return errors
|
||||
|
||||
# MariaDB 11
|
||||
if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010"))
|
||||
elif mariadb_version.major == 11 and mariadb_version.minor == 2:
|
||||
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018"))
|
||||
if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011"))
|
||||
elif mariadb_version.major == 11 and mariadb_version.minor == 1:
|
||||
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019"))
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012"))
|
||||
elif mariadb_version.major == 11 and mariadb_version.minor in [0, 3]: # Demote versions down here once EOL
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A013"))
|
||||
if mariadb_version.major == 11:
|
||||
if mariadb_version.minor == 4 and datetime.now(timezone.utc) > datetime(
|
||||
year=2029, month=5, day=19, tzinfo=timezone.utc
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||
hint=mariadb_download_link,
|
||||
id="allianceauth.checks.A010",
|
||||
)
|
||||
)
|
||||
# Demote versions down here once EOL
|
||||
elif mariadb_version.minor in [0, 1, 2, 3, 5, 6]:
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||
hint=mariadb_download_link,
|
||||
id="allianceauth.checks.A013",
|
||||
)
|
||||
)
|
||||
|
||||
# MariaDB 10
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014"))
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015"))
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=datetime.timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016"))
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017"))
|
||||
elif mariadb_version.major == 10:
|
||||
if mariadb_version.minor == 11 and datetime.now(timezone.utc) > datetime(
|
||||
year=2028, month=2, day=10, tzinfo=timezone.utc
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||
hint=mariadb_download_link,
|
||||
id="allianceauth.checks.A014",
|
||||
)
|
||||
)
|
||||
elif mariadb_version.minor == 6 and datetime.now(timezone.utc) > datetime(
|
||||
year=2026, month=7, day=6, tzinfo=timezone.utc
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||
hint=mariadb_download_link,
|
||||
id="allianceauth.checks.A0015",
|
||||
)
|
||||
)
|
||||
elif mariadb_version.minor == 5 and datetime.now(timezone.utc) > datetime(
|
||||
year=2025, month=6, day=24, tzinfo=timezone.utc
|
||||
):
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||
hint=mariadb_download_link,
|
||||
id="allianceauth.checks.A016",
|
||||
)
|
||||
)
|
||||
# Demote versions down here once EOL
|
||||
elif mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]:
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||
hint=mariadb_download_link,
|
||||
id="allianceauth.checks.A017",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_sqlite(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
"""
|
||||
Check that SQLite is a supported version
|
||||
|
||||
:param app_configs:
|
||||
:type app_configs:
|
||||
:param kwargs:
|
||||
:type kwargs:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "sqlite":
|
||||
try:
|
||||
sqlite_version = Pep440Version(".".join(str(i) for i in sqlite_version_info))
|
||||
sqlite_version = Pep440Version(
|
||||
".".join(str(i) for i in sqlite_version_info)
|
||||
)
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm SQLite Version"))
|
||||
|
||||
return errors
|
||||
if sqlite_version.major == 3 and sqlite_version.minor < 27:
|
||||
errors.append(Error(f"SQLite {sqlite_version.public} Unsupported by Django", hint="https://pkgs.org/download/sqlite3", id="allianceauth.checks.A020"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"SQLite {sqlite_version.public} Unsupported by Django",
|
||||
hint="https://pkgs.org/download/sqlite3",
|
||||
id="allianceauth.checks.A020",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def sql_settings(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
"""
|
||||
Check that SQL settings are correctly configured
|
||||
|
||||
:param app_configs:
|
||||
:type app_configs:
|
||||
:param kwargs:
|
||||
:type kwargs:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "mysql":
|
||||
try:
|
||||
if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4":
|
||||
errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}",
|
||||
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||
id="allianceauth.checks.B001",
|
||||
)
|
||||
)
|
||||
except KeyError:
|
||||
errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}",
|
||||
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||
id="allianceauth.checks.B001",
|
||||
)
|
||||
)
|
||||
|
||||
# This hasn't actually been set on AA yet
|
||||
# try:
|
||||
# if connection.settings_dict["OPTIONS"]["collation"] != "utf8mb4_unicode_ci":
|
||||
# errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
||||
# if (
|
||||
# connection.settings_dict["OPTIONS"]["collation"]
|
||||
# != "utf8mb4_unicode_ci"
|
||||
# ):
|
||||
# errors.append(
|
||||
# Error(
|
||||
# msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}",
|
||||
# hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||
# id="allianceauth.checks.B001",
|
||||
# )
|
||||
# )
|
||||
# except KeyError:
|
||||
# errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
||||
# errors.append(
|
||||
# Error(
|
||||
# msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}",
|
||||
# hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||
# id="allianceauth.checks.B001",
|
||||
# )
|
||||
# )
|
||||
|
||||
# if connection.vendor == "sqlite":
|
||||
|
||||
@@ -162,19 +439,57 @@ def sql_settings(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
|
||||
@register()
|
||||
def celery_settings(app_configs, **kwargs) -> list[CheckMessage]:
|
||||
"""
|
||||
Check that Celery settings are correctly configured
|
||||
|
||||
:param app_configs:
|
||||
:type app_configs:
|
||||
:param kwargs:
|
||||
:type kwargs:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
errors: list[CheckMessage] = []
|
||||
|
||||
try:
|
||||
if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}:
|
||||
errors.append(Error("Celery Priorities are not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
|
||||
if current_app.conf.broker_transport_options != {
|
||||
"priority_steps": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
"queue_order_strategy": "priority",
|
||||
}:
|
||||
errors.append(
|
||||
Error(
|
||||
msg="Celery Priorities are not set correctly",
|
||||
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
|
||||
id="allianceauth.checks.B003",
|
||||
)
|
||||
)
|
||||
except KeyError:
|
||||
errors.append(Error("Celery Priorities are not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg="Celery Priorities are not set",
|
||||
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
|
||||
id="allianceauth.checks.B003",
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
if current_app.conf.broker_connection_retry_on_startup is not True:
|
||||
errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
|
||||
if not current_app.conf.broker_connection_retry_on_startup:
|
||||
errors.append(
|
||||
Error(
|
||||
msg="Celery broker_connection_retry_on_startup not set correctly",
|
||||
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
|
||||
id="allianceauth.checks.B004",
|
||||
)
|
||||
)
|
||||
except KeyError:
|
||||
errors.append(Error("Celery broker_connection_retry_on_startup not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
|
||||
errors.append(
|
||||
Error(
|
||||
msg="Celery broker_connection_retry_on_startup not set",
|
||||
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
|
||||
id="allianceauth.checks.B004",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ def forward(apps, schema_editor):
|
||||
perm.delete()
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
def reverse(apps, schema_editor): # noqa: C901
|
||||
perm_dict = user_permissions_dict(apps)
|
||||
|
||||
corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'corputils/base.html' %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
@@ -238,9 +239,12 @@
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#table-mains').DataTable({
|
||||
"language": {"url": '{{ DT_LANG_PATH }}'},
|
||||
"columnDefs": [
|
||||
{ "sortable": false, "targets": [1] },
|
||||
],
|
||||
@@ -249,6 +253,7 @@
|
||||
});
|
||||
|
||||
$('#table-members').DataTable({
|
||||
"language": {"url": '{{ DT_LANG_PATH }}'},
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
@@ -259,6 +264,7 @@
|
||||
});
|
||||
|
||||
$('#table-unregistered').DataTable({
|
||||
"language": {"url": '{{ DT_LANG_PATH }}'},
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "corputils/base.html" %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block member_data %}
|
||||
@@ -42,9 +43,12 @@
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#table-search').DataTable({
|
||||
"language": {"url": '{{ DT_LANG_PATH }}'},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ def corpstats_add(request, token):
|
||||
|
||||
@login_required
|
||||
@user_passes_test(access_corpstats_test)
|
||||
def corpstats_view(request, corp_id=None):
|
||||
def corpstats_view(request, corp_id=None): # noqa: C901
|
||||
corpstats = None
|
||||
|
||||
# get requested model
|
||||
|
||||
3
allianceauth/crontab/__init__.py
Normal file
3
allianceauth/crontab/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Alliance Auth Crontab Utilities
|
||||
"""
|
||||
14
allianceauth/crontab/apps.py
Normal file
14
allianceauth/crontab/apps.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Crontab App Config
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CrontabConfig(AppConfig):
|
||||
"""
|
||||
Crontab App Config
|
||||
"""
|
||||
|
||||
name = "allianceauth.crontab"
|
||||
label = "crontab"
|
||||
30
allianceauth/crontab/migrations/0001_initial.py
Normal file
30
allianceauth/crontab/migrations/0001_initial.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.16 on 2025-01-20 06:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import allianceauth.crontab.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CronOffset',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('minute', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Minute Offset')),
|
||||
('hour', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Hour Offset')),
|
||||
('day_of_month', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Day of Month Offset')),
|
||||
('month_of_year', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Month of Year Offset')),
|
||||
('day_of_week', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Day of Week Offset')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Cron Offsets',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
allianceauth/crontab/migrations/__init__.py
Normal file
0
allianceauth/crontab/migrations/__init__.py
Normal file
25
allianceauth/crontab/models.py
Normal file
25
allianceauth/crontab/models.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from random import random
|
||||
|
||||
from solo.models import SingletonModel
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def random_default() -> float:
|
||||
return random()
|
||||
|
||||
|
||||
class CronOffset(SingletonModel):
|
||||
|
||||
minute = models.FloatField(_("Minute Offset"), default=random_default)
|
||||
hour = models.FloatField(_("Hour Offset"), default=random_default)
|
||||
day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default)
|
||||
month_of_year = models.FloatField(_("Month of Year Offset"), default=random_default)
|
||||
day_of_week = models.FloatField(_("Day of Week Offset"), default=random_default)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Cron Offsets"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cron Offsets"
|
||||
68
allianceauth/crontab/schedulers.py
Normal file
68
allianceauth/crontab/schedulers.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from celery import schedules
|
||||
from celery.utils.log import get_logger
|
||||
from django_celery_beat.models import CrontabSchedule
|
||||
from django_celery_beat.schedulers import DatabaseScheduler
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
from allianceauth.crontab.models import CronOffset
|
||||
from allianceauth.crontab.utils import offset_cron
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class OffsetDatabaseScheduler(DatabaseScheduler):
|
||||
"""
|
||||
Customization of Django Celery Beat, Database Scheduler
|
||||
Takes the Celery Schedule from local.py and applies our AA Framework Cron Offset, if apply_offset is true
|
||||
Otherwise it passes it through as normal
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def update_from_dict(self, mapping):
|
||||
s = {}
|
||||
|
||||
try:
|
||||
cron_offset = CronOffset.get_solo() # noqa: F841
|
||||
except (OperationalError, ProgrammingError, ObjectDoesNotExist) as exc:
|
||||
# This is just incase we haven't migrated yet or something
|
||||
logger.warning(
|
||||
"OffsetDatabaseScheduler: Could not fetch CronOffset (%r). "
|
||||
"Defering to DatabaseScheduler",
|
||||
exc
|
||||
)
|
||||
return super().update_from_dict(mapping)
|
||||
|
||||
for name, entry_fields in mapping.items():
|
||||
try:
|
||||
apply_offset = entry_fields.pop("apply_offset", False) # Ensure this pops before django tries to save to ORM
|
||||
entry = self.Entry.from_entry(name, app=self.app, **entry_fields)
|
||||
|
||||
if apply_offset:
|
||||
entry_fields.update({"apply_offset": apply_offset}) # Reapply this as its gets pulled from config inconsistently.
|
||||
schedule_obj = entry.schedule
|
||||
if isinstance(schedule_obj, schedules.crontab):
|
||||
offset_cs = CrontabSchedule.from_schedule(offset_cron(schedule_obj))
|
||||
offset_cs, created = CrontabSchedule.objects.get_or_create(
|
||||
minute=offset_cs.minute,
|
||||
hour=offset_cs.hour,
|
||||
day_of_month=offset_cs.day_of_month,
|
||||
month_of_year=offset_cs.month_of_year,
|
||||
day_of_week=offset_cs.day_of_week,
|
||||
timezone=offset_cs.timezone,
|
||||
)
|
||||
entry.schedule = offset_cron(schedule_obj) # This gets passed into Celery Beats Memory, important to keep it in sync with the model/DB
|
||||
entry.model.crontab = offset_cs
|
||||
entry.model.save()
|
||||
logger.debug(f"Offset applied for '{name}' due to 'apply_offset' = True.")
|
||||
|
||||
if entry.model.enabled:
|
||||
s[name] = entry
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error updating schedule for %s: %r", name, e)
|
||||
|
||||
self.schedule.update(s)
|
||||
0
allianceauth/crontab/tests/__init__.py
Normal file
0
allianceauth/crontab/tests/__init__.py
Normal file
64
allianceauth/crontab/tests/test_models.py
Normal file
64
allianceauth/crontab/tests/test_models.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.crontab.models import CronOffset
|
||||
|
||||
|
||||
class CronOffsetModelTest(TestCase):
|
||||
def test_cron_offset_is_singleton(self):
|
||||
"""
|
||||
Test that CronOffset is indeed a singleton and that
|
||||
multiple calls to get_solo() return the same instance.
|
||||
"""
|
||||
offset1 = CronOffset.get_solo()
|
||||
offset2 = CronOffset.get_solo()
|
||||
|
||||
# They should be the exact same object in memory
|
||||
self.assertEqual(offset1.pk, offset2.pk)
|
||||
|
||||
def test_default_values_random(self):
|
||||
"""
|
||||
Test that the default values are set via random_default() when
|
||||
no explicit value is provided. We'll patch 'random.random' to
|
||||
produce predictable output.
|
||||
"""
|
||||
with patch('allianceauth.crontab.models.random', return_value=0.1234):
|
||||
# Force creation of a new CronOffset by clearing the existing one
|
||||
CronOffset.objects.all().delete()
|
||||
|
||||
offset = CronOffset.get_solo() # This triggers creation
|
||||
|
||||
# All fields should be 0.1234, because we patched random()
|
||||
self.assertAlmostEqual(offset.minute, 0.1234)
|
||||
self.assertAlmostEqual(offset.hour, 0.1234)
|
||||
self.assertAlmostEqual(offset.day_of_month, 0.1234)
|
||||
self.assertAlmostEqual(offset.month_of_year, 0.1234)
|
||||
self.assertAlmostEqual(offset.day_of_week, 0.1234)
|
||||
|
||||
def test_update_offset_values(self):
|
||||
"""
|
||||
Test that we can update the offsets and retrieve them.
|
||||
"""
|
||||
offset = CronOffset.get_solo()
|
||||
offset.minute = 0.5
|
||||
offset.hour = 0.25
|
||||
offset.day_of_month = 0.75
|
||||
offset.month_of_year = 0.99
|
||||
offset.day_of_week = 0.33
|
||||
offset.save()
|
||||
|
||||
# Retrieve again to ensure changes persist
|
||||
saved_offset = CronOffset.get_solo()
|
||||
self.assertEqual(saved_offset.minute, 0.5)
|
||||
self.assertEqual(saved_offset.hour, 0.25)
|
||||
self.assertEqual(saved_offset.day_of_month, 0.75)
|
||||
self.assertEqual(saved_offset.month_of_year, 0.99)
|
||||
self.assertEqual(saved_offset.day_of_week, 0.33)
|
||||
|
||||
def test_str_representation(self):
|
||||
"""
|
||||
Verify the __str__ method returns 'Cron Offsets'.
|
||||
"""
|
||||
offset = CronOffset.get_solo()
|
||||
self.assertEqual(str(offset), "Cron Offsets")
|
||||
82
allianceauth/crontab/tests/test_utils.py
Normal file
82
allianceauth/crontab/tests/test_utils.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# myapp/tests/test_tasks.py
|
||||
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from celery.schedules import crontab
|
||||
|
||||
from django.db import ProgrammingError
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.crontab.models import CronOffset
|
||||
from allianceauth.crontab.utils import offset_cron
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestOffsetCron(TestCase):
|
||||
|
||||
def test_offset_cron_normal(self):
|
||||
"""
|
||||
Test that offset_cron modifies the minute/hour fields
|
||||
based on the CronOffset values when everything is normal.
|
||||
"""
|
||||
# We'll create a mock CronOffset instance
|
||||
mock_offset = CronOffset(minute=0.5, hour=0.5)
|
||||
|
||||
# Our initial crontab schedule
|
||||
original_schedule = crontab(
|
||||
minute=[0, 5, 55],
|
||||
hour=[0, 3, 23],
|
||||
day_of_month='*',
|
||||
month_of_year='*',
|
||||
day_of_week='*'
|
||||
)
|
||||
|
||||
# Patch CronOffset.get_solo to return our mock offset
|
||||
with patch('allianceauth.crontab.models.CronOffset.get_solo', return_value=mock_offset):
|
||||
new_schedule = offset_cron(original_schedule)
|
||||
|
||||
# Check the new minute/hour
|
||||
# minute 0 -> 0 + round(60 * 0.5) = 30 % 60 = 30
|
||||
# minute 5 -> 5 + 30 = 35 % 60 = 35
|
||||
# minute 55 -> 55 + 30 = 85 % 60 = 25 --> sorted => 25,30,35
|
||||
self.assertEqual(new_schedule._orig_minute, '25,30,35')
|
||||
|
||||
# hour 0 -> 0 + round(24 * 0.5) = 12 % 24 = 12
|
||||
# hour 3 -> 3 + 12 = 15 % 24 = 15
|
||||
# hour 23 -> 23 + 12 = 35 % 24 = 11 --> sorted => 11,12,15
|
||||
self.assertEqual(new_schedule._orig_hour, '11,12,15')
|
||||
|
||||
# Check that other fields are unchanged
|
||||
self.assertEqual(new_schedule._orig_day_of_month, '*')
|
||||
self.assertEqual(new_schedule._orig_month_of_year, '*')
|
||||
self.assertEqual(new_schedule._orig_day_of_week, '*')
|
||||
|
||||
def test_offset_cron_programming_error(self):
|
||||
"""
|
||||
Test that if a ProgrammingError is raised (e.g. before migrations),
|
||||
offset_cron just returns the original schedule.
|
||||
"""
|
||||
original_schedule = crontab(minute=[0, 15, 30], hour=[1, 2, 3])
|
||||
|
||||
# Force get_solo to raise ProgrammingError
|
||||
with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=ProgrammingError()):
|
||||
new_schedule = offset_cron(original_schedule)
|
||||
|
||||
# Should return the original schedule unchanged
|
||||
self.assertEqual(new_schedule, original_schedule)
|
||||
|
||||
def test_offset_cron_unexpected_exception(self):
|
||||
"""
|
||||
Test that if any other exception is raised, offset_cron
|
||||
also returns the original schedule, and logs the error.
|
||||
"""
|
||||
original_schedule = crontab(minute='0', hour='0')
|
||||
|
||||
# Force get_solo to raise a generic Exception
|
||||
with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=Exception("Something bad")):
|
||||
new_schedule = offset_cron(original_schedule)
|
||||
|
||||
# Should return the original schedule unchanged
|
||||
self.assertEqual(new_schedule, original_schedule)
|
||||
52
allianceauth/crontab/utils.py
Normal file
52
allianceauth/crontab/utils.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
|
||||
from celery.schedules import crontab
|
||||
|
||||
from django.db import ProgrammingError
|
||||
|
||||
from allianceauth.crontab.models import CronOffset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def offset_cron(schedule: crontab) -> crontab:
|
||||
"""Take a crontab and apply a series of precalculated offsets to spread out tasks execution on remote resources
|
||||
|
||||
Args:
|
||||
schedule (crontab): celery.schedules.crontab()
|
||||
|
||||
Returns:
|
||||
crontab: A crontab with offsetted Minute and Hour fields
|
||||
"""
|
||||
|
||||
try:
|
||||
cron_offset = CronOffset.get_solo()
|
||||
|
||||
# Stops this shit from happening 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23
|
||||
# It is only cosmetic, but still annoying
|
||||
if schedule._orig_minute == '*':
|
||||
new_minute = '*'
|
||||
else:
|
||||
new_minute = [(m + (round(60 * cron_offset.minute))) % 60 for m in schedule.minute]
|
||||
if schedule._orig_hour == '*':
|
||||
new_hour = '*'
|
||||
else:
|
||||
new_hour = [(m + (round(24 * cron_offset.hour))) % 24 for m in schedule.hour]
|
||||
|
||||
return crontab(
|
||||
minute=",".join(str(m) for m in sorted(new_minute)),
|
||||
hour=",".join(str(h) for h in sorted(new_hour)),
|
||||
day_of_month=schedule._orig_day_of_month,
|
||||
month_of_year=schedule._orig_month_of_year,
|
||||
day_of_week=schedule._orig_day_of_week)
|
||||
|
||||
except ProgrammingError as e:
|
||||
# If this is called before migrations are run hand back the default schedule
|
||||
# These offsets are stored in a Singleton Model,
|
||||
logger.error(e)
|
||||
return schedule
|
||||
|
||||
except Exception as e:
|
||||
# We absolutely cant fail to hand back a schedule
|
||||
logger.error(e)
|
||||
return schedule
|
||||
@@ -49,8 +49,8 @@ class EveFactionForm(EveEntityForm):
|
||||
def clean_id(self):
|
||||
try:
|
||||
assert self.Meta.model.provider.get_faction(self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound):
|
||||
raise EveEntityNotFoundError('faction', self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound) as e:
|
||||
raise EveEntityNotFoundError('faction', self.cleaned_data['id']) from e
|
||||
if self.Meta.model.objects.filter(faction_id=self.cleaned_data['id']).exists():
|
||||
raise EveEntityExistsError('faction', self.cleaned_data['id'])
|
||||
return self.cleaned_data['id']
|
||||
@@ -70,8 +70,8 @@ class EveCharacterForm(EveEntityForm):
|
||||
def clean_id(self):
|
||||
try:
|
||||
assert self.Meta.model.provider.get_character(self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound):
|
||||
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound) as e:
|
||||
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e
|
||||
if self.Meta.model.objects.filter(character_id=self.cleaned_data['id']).exists():
|
||||
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
|
||||
return self.cleaned_data['id']
|
||||
@@ -90,8 +90,8 @@ class EveCorporationForm(EveEntityForm):
|
||||
def clean_id(self):
|
||||
try:
|
||||
assert self.Meta.model.provider.get_corporation(self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound):
|
||||
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound) as e:
|
||||
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e
|
||||
if self.Meta.model.objects.filter(corporation_id=self.cleaned_data['id']).exists():
|
||||
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
|
||||
return self.cleaned_data['id']
|
||||
@@ -110,8 +110,8 @@ class EveAllianceForm(EveEntityForm):
|
||||
def clean_id(self):
|
||||
try:
|
||||
assert self.Meta.model.provider.get_alliance(self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound):
|
||||
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
|
||||
except (AssertionError, ObjectNotFound) as e:
|
||||
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e
|
||||
if self.Meta.model.objects.filter(alliance_id=self.cleaned_data['id']).exists():
|
||||
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
|
||||
return self.cleaned_data['id']
|
||||
|
||||
@@ -4,7 +4,7 @@ from urllib.parse import quote, urljoin
|
||||
|
||||
from . import _ESI_CATEGORY_ALLIANCE, _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_REGION, _ESI_CATEGORY_SOLARSYSTEM
|
||||
|
||||
_BASE_URL = 'http://evemaps.dotlan.net'
|
||||
_BASE_URL = 'https://evemaps.dotlan.net'
|
||||
|
||||
|
||||
def _build_url(category: str, name: str) -> str:
|
||||
|
||||
@@ -29,29 +29,29 @@ class TestDotlan(TestCase):
|
||||
def test_alliance_url(self):
|
||||
self.assertEqual(
|
||||
dotlan.alliance_url('Wayne Enterprices'),
|
||||
'http://evemaps.dotlan.net/alliance/Wayne_Enterprices'
|
||||
'https://evemaps.dotlan.net/alliance/Wayne_Enterprices'
|
||||
)
|
||||
|
||||
def test_corporation_url(self):
|
||||
self.assertEqual(
|
||||
dotlan.corporation_url('Wayne Technology'),
|
||||
'http://evemaps.dotlan.net/corp/Wayne_Technology'
|
||||
'https://evemaps.dotlan.net/corp/Wayne_Technology'
|
||||
)
|
||||
self.assertEqual(
|
||||
dotlan.corporation_url('Crédit Agricole'),
|
||||
'http://evemaps.dotlan.net/corp/Cr%C3%A9dit_Agricole'
|
||||
'https://evemaps.dotlan.net/corp/Cr%C3%A9dit_Agricole'
|
||||
)
|
||||
|
||||
def test_region_url(self):
|
||||
self.assertEqual(
|
||||
dotlan.region_url('Black Rise'),
|
||||
'http://evemaps.dotlan.net/map/Black_Rise'
|
||||
'https://evemaps.dotlan.net/map/Black_Rise'
|
||||
)
|
||||
|
||||
def test_solar_system_url(self):
|
||||
self.assertEqual(
|
||||
dotlan.solar_system_url('Jita'),
|
||||
'http://evemaps.dotlan.net/system/Jita'
|
||||
'https://evemaps.dotlan.net/system/Jita'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -223,8 +223,8 @@ class EveSwaggerProvider(EveProvider):
|
||||
faction_id=data['faction_id'] if 'faction_id' in data else None,
|
||||
)
|
||||
return model
|
||||
except HTTPNotFound:
|
||||
raise ObjectNotFound(alliance_id, 'alliance')
|
||||
except HTTPNotFound as e:
|
||||
raise ObjectNotFound(alliance_id, 'alliance') from e
|
||||
|
||||
def get_corp(self, corp_id: int) -> Corporation:
|
||||
"""Fetch corporation from ESI."""
|
||||
@@ -240,8 +240,8 @@ class EveSwaggerProvider(EveProvider):
|
||||
faction_id=data['faction_id'] if 'faction_id' in data else None,
|
||||
)
|
||||
return model
|
||||
except HTTPNotFound:
|
||||
raise ObjectNotFound(corp_id, 'corporation')
|
||||
except HTTPNotFound as e:
|
||||
raise ObjectNotFound(corp_id, 'corporation') from e
|
||||
|
||||
def get_character(self, character_id: int) -> Character:
|
||||
"""Fetch character from ESI."""
|
||||
@@ -256,8 +256,8 @@ class EveSwaggerProvider(EveProvider):
|
||||
faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None,
|
||||
)
|
||||
return model
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound):
|
||||
raise ObjectNotFound(character_id, 'character')
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound) as e:
|
||||
raise ObjectNotFound(character_id, 'character') from e
|
||||
|
||||
def _fetch_character_name(self, character_id: int) -> str:
|
||||
"""Fetch character name from ESI."""
|
||||
@@ -288,16 +288,16 @@ class EveSwaggerProvider(EveProvider):
|
||||
return Entity(id=f['faction_id'], name=f['name'])
|
||||
else:
|
||||
raise KeyError()
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity, KeyError):
|
||||
raise ObjectNotFound(faction_id, 'faction')
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity, KeyError) as e:
|
||||
raise ObjectNotFound(faction_id, 'faction') from e
|
||||
|
||||
def get_itemtype(self, type_id: int) -> ItemType:
|
||||
"""Fetch inventory item from ESI."""
|
||||
try:
|
||||
data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result()
|
||||
return ItemType(id=type_id, name=data['name'])
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity):
|
||||
raise ObjectNotFound(type_id, 'type')
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity) as e:
|
||||
raise ObjectNotFound(type_id, 'type') from e
|
||||
|
||||
|
||||
provider = EveSwaggerProvider()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from random import randint
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
@@ -8,7 +9,8 @@ from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TASK_PRIORITY = 7
|
||||
CHUNK_SIZE = 500
|
||||
CHARACTER_AFFILIATION_CHUNK_SIZE = 500
|
||||
EVEONLINE_TASK_JITTER = 600
|
||||
|
||||
|
||||
def chunks(lst, n):
|
||||
@@ -18,13 +20,13 @@ def chunks(lst, n):
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_corp(corp_id):
|
||||
def update_corp(corp_id: int) -> None:
|
||||
"""Update given corporation from ESI"""
|
||||
EveCorporationInfo.objects.update_corporation(corp_id)
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_alliance(alliance_id):
|
||||
def update_alliance(alliance_id: int) -> None:
|
||||
"""Update given alliance from ESI"""
|
||||
EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance()
|
||||
|
||||
@@ -36,23 +38,30 @@ def update_character(character_id: int) -> None:
|
||||
|
||||
|
||||
@shared_task
|
||||
def run_model_update():
|
||||
def run_model_update() -> None:
|
||||
"""Update all alliances, corporations and characters from ESI"""
|
||||
|
||||
#update existing corp models
|
||||
# Queue update tasks for Known Corporation Models
|
||||
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
||||
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
|
||||
update_corp.apply_async(
|
||||
args=[corp['corporation_id']],
|
||||
priority=TASK_PRIORITY,
|
||||
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||
|
||||
# update existing alliance models
|
||||
# Queue update tasks for Known Alliance Models
|
||||
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
|
||||
update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
|
||||
update_alliance.apply_async(
|
||||
args=[alliance['alliance_id']],
|
||||
priority=TASK_PRIORITY,
|
||||
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||
|
||||
# update existing character models
|
||||
# Queue update tasks for Known Character Models
|
||||
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
|
||||
for character_ids_chunk in chunks(character_ids, CHUNK_SIZE):
|
||||
for character_ids_chunk in chunks(character_ids, CHARACTER_AFFILIATION_CHUNK_SIZE):
|
||||
update_character_chunk.apply_async(
|
||||
args=[character_ids_chunk], priority=TASK_PRIORITY
|
||||
)
|
||||
args=[character_ids_chunk],
|
||||
priority=TASK_PRIORITY,
|
||||
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -67,8 +76,9 @@ def update_character_chunk(character_ids_chunk: list):
|
||||
logger.info("Failed to bulk update characters. Attempting single updates")
|
||||
for character_id in character_ids_chunk:
|
||||
update_character.apply_async(
|
||||
args=[character_id], priority=TASK_PRIORITY
|
||||
)
|
||||
args=[character_id],
|
||||
priority=TASK_PRIORITY,
|
||||
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||
return
|
||||
|
||||
affiliations = {
|
||||
@@ -106,5 +116,5 @@ def update_character_chunk(character_ids_chunk: list):
|
||||
|
||||
if corp_changed or alliance_changed or name_changed:
|
||||
update_character.apply_async(
|
||||
args=[character.get('character_id')], priority=TASK_PRIORITY
|
||||
)
|
||||
args=[character.get('character_id')],
|
||||
priority=TASK_PRIORITY)
|
||||
|
||||
@@ -57,11 +57,11 @@ class EsiClientStub:
|
||||
}
|
||||
try:
|
||||
return BravadoOperationStub(data[int(alliance_id)])
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
response = BravadoResponseStub(
|
||||
404, f"Alliance with ID {alliance_id} not found"
|
||||
)
|
||||
raise HTTPNotFound(response)
|
||||
raise HTTPNotFound(response) from e
|
||||
|
||||
@staticmethod
|
||||
def get_alliances_alliance_id_corporations(alliance_id):
|
||||
@@ -87,11 +87,11 @@ class EsiClientStub:
|
||||
}
|
||||
try:
|
||||
return BravadoOperationStub(data[int(character_id)])
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
response = BravadoResponseStub(
|
||||
404, f"Character with ID {character_id} not found"
|
||||
)
|
||||
raise HTTPNotFound(response)
|
||||
raise HTTPNotFound(response) from e
|
||||
|
||||
@staticmethod
|
||||
def post_characters_affiliation(characters: list):
|
||||
@@ -147,11 +147,11 @@ class EsiClientStub:
|
||||
}
|
||||
try:
|
||||
return BravadoOperationStub(data[int(corporation_id)])
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
response = BravadoResponseStub(
|
||||
404, f"Corporation with ID {corporation_id} not found"
|
||||
)
|
||||
raise HTTPNotFound(response)
|
||||
raise HTTPNotFound(response) from e
|
||||
|
||||
class Universe:
|
||||
@staticmethod
|
||||
|
||||
@@ -723,5 +723,5 @@ class TestEveSwaggerProvider(TestCase):
|
||||
my_client = my_provider.client
|
||||
operation = my_client.Universe.get_universe_factions()
|
||||
self.assertEqual(
|
||||
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0'
|
||||
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 dummy@example.net'
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ class TestUpdateTasks(TestCase):
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
@patch('allianceauth.eveonline.tasks.providers')
|
||||
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
|
||||
@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_CHUNK_SIZE', 2)
|
||||
class TestRunModelUpdate(TransactionTestCase):
|
||||
def test_should_run_updates(self, mock_providers, mock_esi_client_factory):
|
||||
# given
|
||||
@@ -139,7 +139,7 @@ class TestRunModelUpdate(TransactionTestCase):
|
||||
@patch('allianceauth.eveonline.tasks.update_character', wraps=update_character)
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
@patch('allianceauth.eveonline.tasks.providers')
|
||||
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
|
||||
@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_CHUNK_SIZE', 2)
|
||||
class TestUpdateCharacterChunk(TestCase):
|
||||
@staticmethod
|
||||
def _updated_character_ids(spy_update_character) -> set:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 1.10.1 on 2016-09-05 22:20
|
||||
|
||||
import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='fatlink',
|
||||
name='fatdatetime',
|
||||
field=models.DateTimeField(default=datetime.datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=datetime.timezone.utc)),
|
||||
field=models.DateTimeField(default=datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=timezone.utc)),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -135,7 +134,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
|
||||
stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True)
|
||||
|
||||
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, 'previous_month': start_of_previous_month, 'corpid': corpid}
|
||||
if datetime.datetime.now() > start_of_next_month:
|
||||
if datetime.datetime.now(timezone.utc) > start_of_next_month:
|
||||
context.update({'next_month': start_of_next_month})
|
||||
|
||||
return render(request, 'fleetactivitytracking/fatlinkstatisticscorpview.html', context=context)
|
||||
@@ -143,7 +142,12 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.fleetactivitytracking_statistics')
|
||||
def fatlink_statistics_view(request, year=datetime.date.today().year, month=datetime.date.today().month):
|
||||
def fatlink_statistics_view(request, year=None, month=None):
|
||||
if year is None:
|
||||
year = datetime.date.today().year
|
||||
if month is None:
|
||||
month = datetime.date.today().month
|
||||
|
||||
year = int(year)
|
||||
month = int(month)
|
||||
start_of_month = datetime.datetime(year, month, 1)
|
||||
@@ -169,16 +173,19 @@ def fatlink_statistics_view(request, year=datetime.date.today().year, month=date
|
||||
stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True)
|
||||
|
||||
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, 'previous_month': start_of_previous_month}
|
||||
if datetime.datetime.now() > start_of_next_month:
|
||||
if datetime.datetime.now(timezone.utc) > start_of_next_month:
|
||||
context.update({'next_month': start_of_next_month})
|
||||
|
||||
return render(request, 'fleetactivitytracking/fatlinkstatisticsview.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
|
||||
def fatlink_personal_statistics_view(request, year=None):
|
||||
if year is None:
|
||||
year = datetime.date.today().year
|
||||
|
||||
year = int(year)
|
||||
logger.debug("Personal statistics view for year %i called by %s" % (year, request.user))
|
||||
logger.debug(f"Personal statistics view for year {year} called by {request.user}")
|
||||
|
||||
user = request.user
|
||||
logger.debug(f"fatlink_personal_statistics_view called by user {request.user}")
|
||||
@@ -194,7 +201,7 @@ def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
|
||||
|
||||
monthlystats = [(i + 1, datetime.date(year, i + 1, 1).strftime("%h"), monthlystats[i]) for i in range(12)]
|
||||
|
||||
if datetime.datetime.now() > datetime.datetime(year + 1, 1, 1):
|
||||
if datetime.datetime.now(timezone.utc) > datetime.datetime(year + 1, 1, 1):
|
||||
context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1, 'next_year': year + 1}
|
||||
else:
|
||||
context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1}
|
||||
@@ -264,7 +271,7 @@ def click_fatlink_view(request, token, fat_hash=None):
|
||||
if character_online["online"] is True:
|
||||
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
|
||||
|
||||
if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
|
||||
if (datetime.now(timezone.utc) - fatlink.fatdatetime) < timedelta(seconds=(fatlink.duration * 60)):
|
||||
if character:
|
||||
# get data
|
||||
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Image overflow fix
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Side Navigation
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
@@ -40,14 +49,14 @@
|
||||
}
|
||||
|
||||
/* Chevron icons */
|
||||
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-down,
|
||||
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-right {
|
||||
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-down,
|
||||
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-right {
|
||||
display: block;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-right,
|
||||
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-down {
|
||||
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-right,
|
||||
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-down {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class GroupsMenuItem(MenuItemHook):
|
||||
MenuItemHook.__init__(
|
||||
self,
|
||||
text=_("Groups"),
|
||||
classes="fa-solid fa-user",
|
||||
classes="fa-solid fa-users",
|
||||
url_name="groupmanagement:groups",
|
||||
order=25,
|
||||
navactive=[
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db import models
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load navactive %}
|
||||
@@ -73,6 +74,8 @@
|
||||
{% include 'bundles/moment-js.html' with locale=True %}
|
||||
{% include 'bundles/filterdropdown-js.html' %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$.fn.dataTable.moment = (format, locale) => {
|
||||
const types = $.fn.dataTable.ext.type;
|
||||
@@ -94,6 +97,7 @@
|
||||
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
|
||||
|
||||
$('#log-entries').DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
order: [[0, 'desc'], [1, 'asc']],
|
||||
filterDropDown:
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
{% load evelinks %}
|
||||
{% load navactive %}
|
||||
@@ -86,9 +87,12 @@
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#tab_group_members').DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
order: [[0, "asc"]],
|
||||
columnDefs: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
@@ -56,7 +57,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if g.group.authgroup.group_leaders.all.count %}
|
||||
{% if g.group.authgroup.group_leader_groups.all.count %}
|
||||
{% for group in g.group.authgroup.group_leader_groups.all %}
|
||||
<span class="my-1 me-1 badge bg-secondary">{{group.name}}</span>
|
||||
{% endfor %}
|
||||
@@ -103,9 +104,13 @@
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#groupsTable').DataTable();
|
||||
$('#groupsTable').DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -87,8 +87,8 @@ def group_membership_audit(request, group_id):
|
||||
logger.warning(f"User {request.user} attempted to view the membership of group {group_id} but permission was denied")
|
||||
raise PermissionDenied
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Group does not exist")
|
||||
except ObjectDoesNotExist as e:
|
||||
raise Http404("Group does not exist") from e
|
||||
render_items = {'group': group}
|
||||
entries = RequestLog.objects.filter(group=group).order_by('-date')
|
||||
render_items['entries'] = entries
|
||||
@@ -117,8 +117,8 @@ def group_membership_list(request, group_id):
|
||||
)
|
||||
raise PermissionDenied
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Group does not exist")
|
||||
except ObjectDoesNotExist as e:
|
||||
raise Http404("Group does not exist") from e
|
||||
|
||||
group_leaders = group.authgroup.group_leaders.all()
|
||||
members = []
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
@@ -111,8 +111,8 @@ def hr_application_view(request, app_id):
|
||||
logger.debug(f"hr_application_view called by user {request.user} for app id {app_id}")
|
||||
try:
|
||||
app = Application.objects.prefetch_related('responses', 'comments', 'comments__user').get(pk=app_id)
|
||||
except Application.DoesNotExist:
|
||||
raise Http404
|
||||
except Application.DoesNotExist as e:
|
||||
raise Http404 from e
|
||||
if request.method == 'POST':
|
||||
if request.user.has_perm('hrapplications.add_applicationcomment'):
|
||||
form = HRApplicationCommentForm(request.POST)
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ class MenuItem(models.Model):
|
||||
)
|
||||
|
||||
# app related properties
|
||||
hook_hash = models.CharField(
|
||||
hook_hash = models.CharField( # noqa: DJ001
|
||||
max_length=64,
|
||||
default=None,
|
||||
null=True,
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
data-bs-target="#{{ item.html_id }}"
|
||||
aria-expanded="false"
|
||||
aria-controls=""
|
||||
{% endif %}>
|
||||
</i>
|
||||
{% endif %}
|
||||
></i>
|
||||
<a
|
||||
class="nav-link flex-fill align-self-center me-auto {% if item.navactive %}{% navactive request item.navactive|join:' ' %}{% endif %}"
|
||||
{% if item.is_folder %}
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
{% if item.is_folder %}
|
||||
<span
|
||||
class="pill m-2 align-self-center collapsed"
|
||||
class="pill m-2 align-self-center"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ item.html_id }}"
|
||||
|
||||
@@ -77,7 +77,7 @@ class RenderedMenuItem:
|
||||
self.html_id = hook_obj.html_id
|
||||
|
||||
|
||||
def render_menu(request: HttpRequest) -> list[RenderedMenuItem]:
|
||||
def render_menu(request: HttpRequest) -> list[RenderedMenuItem]: # noqa: C901
|
||||
"""Return the rendered side menu for including in a template.
|
||||
|
||||
This function is creating BS5 style menus.
|
||||
|
||||
@@ -68,35 +68,3 @@ class TestDefaultDashboardWithSideMenu(TestCase):
|
||||
self.assertEqual(links["/dashboard/"], "Dashboard")
|
||||
self.assertEqual(links["/groups/"], "Groups")
|
||||
self.assertNotIn("http://www.example.com/alpha", links)
|
||||
|
||||
|
||||
class TestBS3DashboardWithSideMenu(TestCase):
|
||||
def test_should_not_show_group_management_when_user_has_no_permission(self):
|
||||
# given
|
||||
user = create_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
# when
|
||||
response = self.client.get("/dashboard_bs3/")
|
||||
|
||||
# then
|
||||
self.assertEqual(response.status_code, HTTPStatus.OK)
|
||||
links = extract_links(response)
|
||||
self.assertEqual(links["/dashboard/"], "Dashboard")
|
||||
self.assertEqual(links["/groups/"], "Groups")
|
||||
self.assertNotIn("/groupmanagement/requests/", links)
|
||||
|
||||
def test_should_show_group_management_when_user_has_permission(self):
|
||||
# given
|
||||
user = create_user(permissions=["auth.group_management"])
|
||||
self.client.force_login(user)
|
||||
|
||||
# when
|
||||
response = self.client.get("/dashboard_bs3/")
|
||||
|
||||
# then
|
||||
self.assertEqual(response.status_code, HTTPStatus.OK)
|
||||
links = extract_links(response)
|
||||
self.assertEqual(links["/dashboard/"], "Dashboard")
|
||||
self.assertEqual(links["/groups/"], "Groups")
|
||||
self.assertEqual(links["/groupmanagement/requests/"], "Group Management")
|
||||
|
||||
@@ -1 +1,21 @@
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allianceauth.notifications.models import Notification
|
||||
|
||||
|
||||
def notify_user_view(request):
|
||||
'''Simple view sending a notification to the user'''
|
||||
|
||||
Notification.objects.notify_user(
|
||||
user=request.user,
|
||||
title="Some title",
|
||||
message="Some message",
|
||||
level=Notification.Level.INFO,
|
||||
)
|
||||
|
||||
"""
|
||||
from .core import notify # noqa: F401
|
||||
|
||||
@@ -15,7 +15,7 @@ class OpForm(forms.Form):
|
||||
operation_name = forms.CharField(max_length=254, required=True, label=_("Operation Name"))
|
||||
type = forms.CharField(required=False, label=_("Operation Type"))
|
||||
fc = forms.CharField(max_length=254, required=True, label=_("Fleet Commander"))
|
||||
duration = forms.CharField(max_length=254, required=True, label=_("Duration"))
|
||||
duration = forms.CharField(max_length=25, required=True, label=_("Duration"))
|
||||
description = forms.CharField(
|
||||
widget=forms.Textarea(attrs={"rows": 10, "cols": 20, "input_type": "textarea"}),
|
||||
required=False,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
@@ -57,11 +58,14 @@
|
||||
{% include "bundles/datatables-js-bs5.html" %}
|
||||
{% include "bundles/filterdropdown-js.html" %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
const groupColumn = 0;
|
||||
|
||||
$('#tab_permissions_audit').DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
columnDefs: [{
|
||||
"visible": false,
|
||||
"targets": groupColumn
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
@@ -62,11 +63,14 @@
|
||||
{% include "bundles/datatables-js-bs5.html" %}
|
||||
{% include "bundles/filterdropdown-js.html" %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
const groupColumn = 0;
|
||||
|
||||
$('#tab_permissions_overview').DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
columnDefs: [{
|
||||
"visible": false,
|
||||
"targets": groupColumn
|
||||
|
||||
@@ -48,8 +48,8 @@ def permissions_audit(request, app_label, model, codename):
|
||||
.prefetch_related('group_set', 'user_set', 'state_set',
|
||||
'state_set__userprofile_set', 'group_set__user_set', 'state_set__userprofile_set__user')\
|
||||
.get(content_type__app_label=app_label, content_type__model=model, codename=codename)
|
||||
except Permission.DoesNotExist:
|
||||
raise Http404
|
||||
except Permission.DoesNotExist as e:
|
||||
raise Http404 from e
|
||||
|
||||
context = {'permission': {
|
||||
'permission': perm,
|
||||
|
||||
@@ -42,13 +42,16 @@ INSTALLED_APPS = [
|
||||
'allianceauth.theme.flatly',
|
||||
'allianceauth.theme.materia',
|
||||
"allianceauth.custom_css",
|
||||
'allianceauth.crontab',
|
||||
'sri',
|
||||
]
|
||||
|
||||
SRI_ALGORITHM = "sha512"
|
||||
SECRET_KEY = "wow I'm a really bad default secret key"
|
||||
|
||||
# Celery configuration
|
||||
BROKER_URL = 'redis://localhost:6379/0'
|
||||
CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
|
||||
CELERYBEAT_SCHEDULER = "allianceauth.crontab.schedulers.OffsetDatabaseScheduler"
|
||||
CELERYBEAT_SCHEDULE = {
|
||||
'esi_cleanup_callbackredirect': {
|
||||
'task': 'esi.tasks.cleanup_callbackredirect',
|
||||
@@ -61,10 +64,12 @@ CELERYBEAT_SCHEDULE = {
|
||||
'run_model_update': {
|
||||
'task': 'allianceauth.eveonline.tasks.run_model_update',
|
||||
'schedule': crontab(minute='0', hour="*/6"),
|
||||
'apply_offset': True
|
||||
},
|
||||
'check_all_character_ownership': {
|
||||
'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
|
||||
'schedule': crontab(minute='0', hour='*/4'),
|
||||
'apply_offset': True
|
||||
},
|
||||
'analytics_daily_stats': {
|
||||
'task': 'allianceauth.analytics.tasks.analytics_daily_stats',
|
||||
@@ -72,6 +77,7 @@ CELERYBEAT_SCHEDULE = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
@@ -110,6 +116,46 @@ LANGUAGES = ( # Sorted by Language Code alphabetical order + English at top
|
||||
("zh-hans", _("Simplified Chinese")),
|
||||
)
|
||||
|
||||
# Django's language codes are different from some of the libraries we use,
|
||||
# so we need to map them.
|
||||
# When adding a new language, please remember to add it to the mapping
|
||||
# and add the language files to their respective directories under `allianceauth/static/allianceauth/libs/`.
|
||||
LANGUAGE_MAPPING = {
|
||||
# See https://github.com/DataTables/Plugins/tree/master/i18n for available languages
|
||||
# (We use the JSON files)
|
||||
# `allianceauth/static/allianceauth/libs/DataTables/Plugins/{version}/i18n/` for the files
|
||||
"DataTables": {
|
||||
"cs-cz": "cs",
|
||||
"de": "de-DE",
|
||||
"es": "es-ES",
|
||||
"fr-fr": "fr-FR",
|
||||
"it-it": "it-IT",
|
||||
"ja": "ja",
|
||||
"ko-kr": "ko",
|
||||
"nl-nl": "nl-NL",
|
||||
"pl-pl": "pl",
|
||||
"ru": "ru",
|
||||
"uk": "uk",
|
||||
"zh-hans": "zh-HANT",
|
||||
},
|
||||
# See https://github.com/moment/moment/tree/master/locale for available languages
|
||||
# `allianceauth/static/allianceauth/libs/moment.js/{version}/locale/` for the files
|
||||
"MomentJS": {
|
||||
"cs-cz": "cs",
|
||||
"de": "de",
|
||||
"es": "es",
|
||||
"fr-fr": "fr",
|
||||
"it-it": "it",
|
||||
"ja": "ja",
|
||||
"ko-kr": "ko",
|
||||
"nl-nl": "nl",
|
||||
"pl-pl": "pl",
|
||||
"ru": "ru",
|
||||
"uk": "uk",
|
||||
"zh-hans": "zh-cn",
|
||||
},
|
||||
}
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
|
||||
@@ -33,7 +33,6 @@ class AbstractServiceModel(models.Model):
|
||||
related_name='%(app_label)s'
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -85,8 +84,8 @@ class ServicesCRUDMixin(SingleObjectMixin):
|
||||
|
||||
try:
|
||||
return queryset.get(user__pk=self.request.user.pk)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404
|
||||
except ObjectDoesNotExist as e:
|
||||
raise Http404 from e
|
||||
|
||||
|
||||
class BaseDeactivateServiceAccountView(ServicesCRUDMixin, BaseServiceView, DeleteView):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
of the current Alliance Auth instance.
|
||||
|
||||
Example
|
||||
=======
|
||||
-------
|
||||
|
||||
Here is an example for using the api to fetch the current roles from the configured Discord server.
|
||||
|
||||
|
||||
@@ -44,26 +44,24 @@ MAX_JITTER_PER_RUN_SECS = 1.0
|
||||
|
||||
def worker(num: int):
|
||||
"""worker function"""
|
||||
worker_info = 'worker %d' % num
|
||||
logger.info('%s: started', worker_info)
|
||||
worker_info = f'worker {num}'
|
||||
logger.info(f'{worker_info}: started')
|
||||
client = DiscordClient(DISCORD_BOT_TOKEN)
|
||||
try:
|
||||
runs = 0
|
||||
while runs < NUMBER_OF_RUNS:
|
||||
run_info = '%s: run %d' % (worker_info, runs + 1)
|
||||
run_info = f'{worker_info}: run {runs + 1}'
|
||||
my_jitter_secs = random() * MAX_JITTER_PER_RUN_SECS
|
||||
logger.info('%s - waiting %s secs', run_info, f'{my_jitter_secs:.3f}')
|
||||
logger.info(f'{run_info} - waiting {my_jitter_secs:.3f} secs')
|
||||
sleep(my_jitter_secs)
|
||||
logger.info('%s - started', run_info)
|
||||
logger.info(f'{run_info} - started')
|
||||
try:
|
||||
client.modify_guild_member(
|
||||
DISCORD_GUILD_ID, DISCORD_USER_ID, nick=NICK
|
||||
)
|
||||
runs += 1
|
||||
except DiscordApiBackoff as bo:
|
||||
message = '%s - waiting out API backoff for %d ms' % (
|
||||
run_info, bo.retry_after
|
||||
)
|
||||
message = f'{run_info} - waiting out API backoff for {bo.retry_after} ms'
|
||||
logger.info(message)
|
||||
print()
|
||||
print(message)
|
||||
|
||||
@@ -89,12 +89,12 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
|
||||
bo,
|
||||
bo.retry_after_seconds
|
||||
)
|
||||
raise self.retry(countdown=bo.retry_after_seconds)
|
||||
raise self.retry(countdown=bo.retry_after_seconds) from bo
|
||||
|
||||
except AttributeError:
|
||||
raise ValueError(f'{method} not a valid method for DiscordUser')
|
||||
except AttributeError as e:
|
||||
raise ValueError(f'{method} not a valid method for DiscordUser') from e
|
||||
|
||||
except (HTTPError, ConnectionError):
|
||||
except (HTTPError, ConnectionError) as e:
|
||||
logger.warning(
|
||||
'%s failed for user %s, retrying in %d secs',
|
||||
method,
|
||||
@@ -103,7 +103,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
|
||||
exc_info=True
|
||||
)
|
||||
if self.request.retries < DISCORD_TASKS_MAX_RETRIES:
|
||||
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE)
|
||||
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE) from e
|
||||
else:
|
||||
logger.error(
|
||||
'%s failed for user %s after max retries',
|
||||
@@ -192,8 +192,8 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any:
|
||||
try:
|
||||
result = getattr(DiscordUser.objects, method)(**kwargs)
|
||||
|
||||
except AttributeError:
|
||||
raise ValueError(f'{method} not a valid method for DiscordUser.objects')
|
||||
except AttributeError as e:
|
||||
raise ValueError(f'{method} not a valid method for DiscordUser.objects') from e
|
||||
|
||||
except DiscordApiBackoff as bo:
|
||||
logger.info(
|
||||
@@ -202,9 +202,9 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any:
|
||||
bo,
|
||||
bo.retry_after_seconds
|
||||
)
|
||||
raise self.retry(countdown=bo.retry_after_seconds)
|
||||
raise self.retry(countdown=bo.retry_after_seconds) from bo
|
||||
|
||||
except (HTTPError, ConnectionError):
|
||||
except (HTTPError, ConnectionError) as e:
|
||||
logger.warning(
|
||||
'%s failed, retrying in %d secs',
|
||||
method,
|
||||
@@ -212,7 +212,7 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any:
|
||||
exc_info=True
|
||||
)
|
||||
if self.request.retries < DISCORD_TASKS_MAX_RETRIES:
|
||||
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE)
|
||||
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE) from e
|
||||
else:
|
||||
logger.error('%s failed after max retries', method, exc_info=True)
|
||||
|
||||
|
||||
@@ -108,9 +108,8 @@ class DiscourseManager:
|
||||
providers.discourse.client.activate(u_id)
|
||||
|
||||
@staticmethod
|
||||
def __update_user(username, **kwargs):
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
providers.discourse.client.update_user(endpoint, u_id, **kwargs)
|
||||
def __update_user(username, **kwargs) -> None:
|
||||
providers.discourse.client.update_user(username, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def __create_user(username, email, password):
|
||||
|
||||
@@ -52,7 +52,7 @@ class DiscourseTasks:
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.warning(f"Discourse group sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
logger.debug(f"Updated user {user} discourse groups.")
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 4.2.16 on 2024-10-07 13:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mumble", "0012_mumble_client_info"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="mumbleuser",
|
||||
options={
|
||||
"permissions": (
|
||||
("access_mumble", "Can access the Mumble service"),
|
||||
(
|
||||
"view_connection_history",
|
||||
"Can access the connection history of the Mumble service",
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="mumbleuser",
|
||||
name="release",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
editable=False,
|
||||
help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.",
|
||||
max_length=254,
|
||||
null=True,
|
||||
verbose_name="Mumble Release",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -89,7 +89,7 @@ class MumbleUser(AbstractServiceModel):
|
||||
max_length=254,
|
||||
blank=True,
|
||||
editable=False,
|
||||
help_text="The Mumble Release the user last authenticated with"
|
||||
help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else."
|
||||
)
|
||||
version = models.IntegerField(
|
||||
verbose_name="Mumble Version",
|
||||
@@ -157,4 +157,5 @@ class MumbleUser(AbstractServiceModel):
|
||||
class Meta:
|
||||
permissions = (
|
||||
("access_mumble", "Can access the Mumble service"),
|
||||
("view_connection_history", "Can access the connection history of the Mumble service"),
|
||||
)
|
||||
|
||||
@@ -41,9 +41,9 @@ class MumbleTasks:
|
||||
return True
|
||||
except MumbleUser.DoesNotExist:
|
||||
logger.info(f"Mumble group sync failed for {user}, user does not have a mumble account")
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"Mumble group sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
else:
|
||||
logger.debug(f"User {user} does not have a mumble account, skipping")
|
||||
return False
|
||||
@@ -61,9 +61,9 @@ class MumbleTasks:
|
||||
return True
|
||||
except MumbleUser.DoesNotExist:
|
||||
logger.info(f"Mumble display name sync failed for {user}, user does not have a mumble account")
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"Mumble display name sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
else:
|
||||
logger.debug(f"User {user} does not have a mumble account, skipping")
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load aa_i18n %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
{% translate "Mumble" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% trans "Mumble History" %} - {{ mumble_url }}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block header_nav_collapse_left %}
|
||||
{% endblock header_nav_collapse_left %}
|
||||
|
||||
{% block header_nav_collapse_right %}
|
||||
{% endblock header_nav_collapse_right %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card col-lg-12 mb-3">
|
||||
<div class="card-header">
|
||||
<span class="card-title">{% translate "Server Connection History" %}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table w-100" id="table-mumble-connection-history">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start">{% translate "User" %}</th>
|
||||
<th class="text-start">{% translate "Displayed Name" %}</th>
|
||||
<th class="text-start">{% translate "Release" %}</th>
|
||||
<th class="text-start">{% translate "Version" %}</th>
|
||||
<th class="text-end">{% translate "Last Connect" %}</th>
|
||||
<th class="text-end">{% translate "Last Disconnect" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">{% translate "Server Connection Breakdown" %}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<canvas id="pieChart"></canvas> <!-- Canvas element for the pie chart -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">{% translate "Server Connection Breakdown" %}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table w-100" id="table-mumble-connection-stats">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start">{% translate "Version" %}</th>
|
||||
<th class="text-end">{% translate "Number" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include "bundles/datatables-js-bs5.html" %}
|
||||
{% include "bundles/filterdropdown-js.html" %}
|
||||
{% include "bundles/chart-js.html" %}
|
||||
{% include "bundles/moment-js.html" with locale=True %}
|
||||
|
||||
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
const MUMBLESTATS_DATETIME_FORMAT = 'LLL';
|
||||
|
||||
'use strict';
|
||||
|
||||
$("#table-mumble-connection-history").DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
ajax: {
|
||||
url: '{% url "mumble:connection_history_data" %}',
|
||||
dataSrc: 'connection_history_data',
|
||||
},
|
||||
columns: [
|
||||
{ data: 'user' },
|
||||
{ data: 'display_name' },
|
||||
{ data: 'release' },
|
||||
{ data: 'version' },
|
||||
{
|
||||
data: 'last_connect',
|
||||
render: {
|
||||
_: (data) => {
|
||||
return data === null
|
||||
? ''
|
||||
: moment(data)
|
||||
.utc()
|
||||
.format(MUMBLESTATS_DATETIME_FORMAT);
|
||||
},
|
||||
sort: (data) => {
|
||||
return data === null ? '' : data;
|
||||
}
|
||||
},
|
||||
className: 'text-end',
|
||||
},
|
||||
{
|
||||
data: 'last_disconnect',
|
||||
render: {
|
||||
_: (data) => {
|
||||
return data === null
|
||||
? ''
|
||||
: moment(data)
|
||||
.utc()
|
||||
.format(MUMBLESTATS_DATETIME_FORMAT);
|
||||
},
|
||||
sort: (data) => {
|
||||
return data === null ? '' : data;
|
||||
}
|
||||
},
|
||||
className: 'text-end',
|
||||
},
|
||||
],
|
||||
order: [[4, 'desc']],
|
||||
processing: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
filterDropDown: {
|
||||
columns: [
|
||||
{
|
||||
idx: 2,
|
||||
},
|
||||
{
|
||||
idx: 3,
|
||||
},
|
||||
],
|
||||
bootstrap: true,
|
||||
bootstrap_version: 5,
|
||||
},
|
||||
});
|
||||
|
||||
$("#table-mumble-connection-stats").DataTable({
|
||||
language: {url: '{{ DT_LANG_PATH }}'},
|
||||
ajax: {
|
||||
url: '{% url "mumble:release_counts_data" %}',
|
||||
dataSrc: 'release_counts_data',
|
||||
},
|
||||
columns: [
|
||||
{ data: 'release' },
|
||||
{ data: 'user_count', className: 'text-end' },
|
||||
],
|
||||
order: [[1, 'desc']],
|
||||
processing: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
});
|
||||
|
||||
// Initialize empty Pie chart
|
||||
const elementBody = document.querySelector('body');
|
||||
const elementBodyCss = getComputedStyle(elementBody);
|
||||
const ctx = document.getElementById('pieChart').getContext('2d');
|
||||
const pieChart = new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: [], // Initially empty
|
||||
datasets: [
|
||||
{
|
||||
label: 'Server Connection Breakdown',
|
||||
data: [], // Initially empty
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)',
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
'rgba(255, 206, 86, 0.2)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255, 99, 132, 1)',
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Chart.defaults.color = elementBodyCss.color;
|
||||
|
||||
// AJAX call to dynamically update the chart
|
||||
$.ajax({
|
||||
url: '{% url "mumble:release_pie_chart_data" %}', // Your Django view URL that returns chart data
|
||||
method: "GET",
|
||||
success: (data) => {
|
||||
// Replace chart data with the data from the AJAX response
|
||||
pieChart.data.labels = data.labels; // Set the new labels
|
||||
pieChart.data.datasets[0].data = data.values; // Set the new values
|
||||
|
||||
// Update the chart to reflect the new data
|
||||
pieChart.update();
|
||||
},
|
||||
error: (xhr, status, error) => {
|
||||
console.error('Error fetching pie chart data:', status, error);
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include "bundles/datatables-css-bs5.html" %}
|
||||
{% endblock extra_css %}
|
||||
@@ -48,4 +48,9 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a class="btn btn-primary" type="button" id="btnMumbleConnectionHistory" href="{% url 'mumble:connection_history' %}" title="{% translate 'Mumble Connection History' %}">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i> History
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,6 +10,10 @@ module_urls = [
|
||||
path('deactivate/', views.DeleteMumbleView.as_view(), name='deactivate'),
|
||||
path('reset_password/', views.ResetPasswordMumbleView.as_view(), name='reset_password'),
|
||||
path('set_password/', views.SetPasswordMumbleView.as_view(), name='set_password'),
|
||||
path('connection_history/', views.connection_history, name="connection_history"),
|
||||
path('ajax/connection_history_data', views.connection_history_data, name="connection_history_data"),
|
||||
path('ajax/release_counts_data', views.release_counts_data, name="release_counts_data"),
|
||||
path('ajax/release_pie_chart_data', views.release_pie_chart_data, name="release_pie_chart_data"),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
from allianceauth.services.abstract import (
|
||||
BaseCreatePasswordServiceAccountView,
|
||||
BaseDeactivateServiceAccountView,
|
||||
@@ -39,3 +45,51 @@ class ResetPasswordMumbleView(MumbleViewMixin, BaseResetPasswordServiceAccountVi
|
||||
|
||||
class SetPasswordMumbleView(MumbleViewMixin, BaseSetPasswordServiceAccountView):
|
||||
form_class = MumblePasswordForm
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('mumble.view_connection_history')
|
||||
def connection_history(request) -> HttpResponse:
|
||||
|
||||
context = {
|
||||
"mumble_url": settings.MUMBLE_URL,
|
||||
}
|
||||
|
||||
return render(request, 'services/mumble/mumble_connection_history.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("mumble.view_connection_history")
|
||||
def connection_history_data(request) -> JsonResponse:
|
||||
connection_history_data = MumbleUser.objects.all(
|
||||
).values(
|
||||
'user',
|
||||
'display_name',
|
||||
'release',
|
||||
'version',
|
||||
'last_connect',
|
||||
'last_disconnect',
|
||||
)
|
||||
|
||||
return JsonResponse({"connection_history_data": list(connection_history_data)})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("mumble.view_connection_history")
|
||||
def release_counts_data(request) -> JsonResponse:
|
||||
release_counts_data = MumbleUser.objects.values('release').annotate(user_count=Count('user_id')).order_by('release')
|
||||
|
||||
return JsonResponse({
|
||||
"release_counts_data": list(release_counts_data),
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("mumble.view_connection_history")
|
||||
def release_pie_chart_data(request) -> JsonResponse:
|
||||
release_counts = MumbleUser.objects.values('release').annotate(user_count=Count('user_id')).order_by('release')
|
||||
|
||||
return JsonResponse({
|
||||
"labels": list(release_counts.values_list("release", flat=True)),
|
||||
"values": list(release_counts.values_list("user_count", flat=True)),
|
||||
})
|
||||
|
||||
@@ -54,9 +54,9 @@ class OpenfireTasks:
|
||||
logger.debug(f"Updating user {user} jabber groups to {groups}")
|
||||
try:
|
||||
OpenfireManager.update_user_groups(user.openfire.username, groups)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"Jabber group sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
logger.debug(f"Updated user {user} jabber groups.")
|
||||
else:
|
||||
logger.debug("User does not have an openfire account")
|
||||
|
||||
@@ -112,7 +112,7 @@ def jabber_broadcast_view(request):
|
||||
|
||||
OpenfireManager.send_broadcast_message(group_to_send, message_to_send)
|
||||
|
||||
messages.success(request, _('Sent Jabber broadcast to {}'.format(group_to_send)))
|
||||
messages.success(request, _(f'Sent Jabber broadcast to {group_to_send}'))
|
||||
logger.info(f"Sent Jabber broadcast on behalf of user {request.user}")
|
||||
except PingBotException as e:
|
||||
messages.error(request, e)
|
||||
|
||||
@@ -8,10 +8,10 @@ class Phpbb3User(models.Model):
|
||||
related_name='phpbb3')
|
||||
username = models.CharField(max_length=254)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("access_phpbb3", "Can access the phpBB3 service"),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.username
|
||||
|
||||
@@ -49,9 +49,9 @@ class Phpbb3Tasks:
|
||||
logger.debug(f"Updating user {user} phpbb3 groups to {groups}")
|
||||
try:
|
||||
Phpbb3Manager.update_groups(user.phpbb3.username, groups)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"Phpbb group sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
logger.debug(f"Updated user {user} phpbb3 groups.")
|
||||
else:
|
||||
logger.debug("User does not have a Phpbb3 account")
|
||||
|
||||
@@ -53,9 +53,9 @@ class SmfTasks:
|
||||
logger.debug(f"Updating user {user} smf groups to {groups}")
|
||||
try:
|
||||
SmfManager.update_groups(user.smf.username, groups)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"smf group sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
logger.debug(f"Updated user {user} smf groups.")
|
||||
else:
|
||||
logger.debug("User does not have an smf account")
|
||||
@@ -77,11 +77,11 @@ class SmfTasks:
|
||||
f"SMF displayed name sync failed for {user}, "
|
||||
"user does not have a SMF account"
|
||||
)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"SMF displayed name sync failed for {user}, retrying in 10 mins"
|
||||
)
|
||||
raise self.retry(countdown=60 * 10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
else:
|
||||
logger.debug(f"User {user} does not have a SMF account, skipping")
|
||||
|
||||
|
||||
@@ -17,9 +17,10 @@ class Teamspeak3User(models.Model):
|
||||
("access_teamspeak3", "Can access the Teamspeak3 service"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.uid
|
||||
|
||||
|
||||
class TSgroup(models.Model):
|
||||
ts_group_id = models.IntegerField(primary_key=True)
|
||||
ts_group_name = models.CharField(max_length=30)
|
||||
@@ -27,7 +28,7 @@ class TSgroup(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'TS Group'
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.ts_group_name
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class AuthTS(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'Auth / TS Group'
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.auth_group.name
|
||||
|
||||
|
||||
@@ -49,7 +50,7 @@ class UserTSgroup(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'User TS Group'
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.user.name
|
||||
|
||||
|
||||
@@ -57,5 +58,5 @@ class StateGroup(models.Model):
|
||||
state = models.ForeignKey(State, on_delete=models.CASCADE)
|
||||
ts_group = models.ForeignKey(TSgroup, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.pk
|
||||
def __str__(self) -> str:
|
||||
return str(self.pk)
|
||||
|
||||
@@ -81,7 +81,7 @@ class Teamspeak3Tasks:
|
||||
logger.debug(f"Updated user {user} teamspeak3 groups.")
|
||||
except TeamspeakError as e:
|
||||
logger.error(f"Error occured while syncing TS groups for {user}: {str(e)}")
|
||||
raise self.retry(countdown=60*10)
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
else:
|
||||
logger.debug("User does not have a teamspeak3 account")
|
||||
|
||||
|
||||
@@ -116,8 +116,7 @@ class Teamspeak3HooksTestCase(TestCase):
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
_ = User.objects.get(username=self.member)
|
||||
|
||||
_ = User.objects.get(username=self.member).teamspeak3
|
||||
|
||||
def test_render_services_ctrl(self):
|
||||
service = self.service()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user