Compare commits

..

3 Commits

Author SHA1 Message Date
Ariel Rin
3a1e0d6482 Merge branch 'add-missing-images' into 'x-editable-bs5'
[ADD] Missing images for x-editable

See merge request soratidus999/allianceauth!11
2024-09-23 23:32:53 +00:00
Peter Pfeufer
d78261d03f [ADD] Missing images for x-editable 2024-09-16 11:01:41 +02:00
Joel Falknau
4d98136800 BS5 Fork of x-editable 2024-09-16 14:46:47 +10:00
202 changed files with 3666 additions and 10377 deletions

View File

@@ -98,6 +98,7 @@ test-3.13-core:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
allow_failure: true
test-3.10-all: test-3.10-all:
<<: *only-default <<: *only-default
@@ -147,6 +148,7 @@ test-3.13-all:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
allow_failure: true
build-test: build-test:
stage: test stage: test

View File

@@ -3,42 +3,22 @@
# Update this file: # Update this file:
# pre-commit autoupdate # 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: repos:
# Code Upgrades - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4
hooks:
# Run the linter, and only the linter
- id: ruff
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.25.0 rev: 1.21.0
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: [--target-version=5.2] args: [--target-version=4.2]
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args: [--py310-plus]
# Formatting # Formatting
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v4.6.0
hooks: hooks:
# Identify invalid files # Identify invalid files
- id: check-ast - id: check-ast
@@ -53,9 +33,9 @@ repos:
- id: detect-private-key - id: detect-private-key
- id: check-case-conflict - id: check-case-conflict
# Python checks # Python checks
# - id: check-docstring-first # - id: check-docstring-first
- id: debug-statements - id: debug-statements
# - id: requirements-txt-fixer # - id: requirements-txt-fixer
- id: fix-encoding-pragma - id: fix-encoding-pragma
args: [--remove] args: [--remove]
- id: fix-byte-order-marker - id: fix-byte-order-marker
@@ -64,34 +44,56 @@ repos:
args: [--fix=lf] args: [--fix=lf]
- id: trailing-whitespace - id: trailing-whitespace
args: [--markdown-linebreak-ext=md] args: [--markdown-linebreak-ext=md]
exclude: |
(?x)(
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json
)
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: |
(?x)(
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json
)
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python - repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 3.2.1 rev: 3.0.3
hooks: hooks:
- id: editorconfig-checker - 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 - repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0 rev: v0.41.0
hooks: hooks:
- id: markdownlint - id: markdownlint
language: node
args: args:
- --disable=MD013 - --disable=MD013
# Infrastructure # Infrastructure
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.6.0 rev: 2.2.3
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
name: pyproject.toml formatter
description: "Format the pyproject.toml file."
args: args:
- --indent=4 - --indent=4
additional_dependencies: additional_dependencies:
- tox==4.26.0 # https://github.com/tox-dev/tox/releases/latest - tox==4.18.1 # https://github.com/tox-dev/tox/releases/latest
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0
hooks:
- id: tox-ini-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1 rev: v0.19
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
name: Validate pyproject.toml
description: "Validate the pyproject.toml file."

View File

@@ -5,7 +5,7 @@
[![django](https://img.shields.io/pypi/djversions/allianceauth?label=django)](https://pypi.org/project/allianceauth/) [![django](https://img.shields.io/pypi/djversions/allianceauth?label=django)](https://pypi.org/project/allianceauth/)
[![version](https://img.shields.io/pypi/v/allianceauth?label=release)](https://pypi.org/project/allianceauth/) [![version](https://img.shields.io/pypi/v/allianceauth?label=release)](https://pypi.org/project/allianceauth/)
[![pipeline status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master) [![pipeline status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](https://allianceauth.readthedocs.io/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest)
[![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master) [![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk) [![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk)
@@ -14,7 +14,7 @@ An auth system for EVE Online to help in-game organizations manage online servic
## Content ## Content
- [Overview](#overview) - [Overview](#overview)
- [Documentation](https://allianceauth.rtfd.io) - [Documentation](http://allianceauth.rtfd.io)
- [Support](#support) - [Support](#support)
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases) - [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
- [Developer Team](#development-team) - [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 - 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](https://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](http://allianceauth.rtfd.io).
## Screenshot ## Screenshot

View File

@@ -5,7 +5,7 @@ manage online service access.
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '5.0.0a3' __version__ = '4.3.1'
__title__ = 'AllianceAuth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}' NAME = f'{__title__} v{__version__}'

View File

@@ -1,17 +1,15 @@
from solo.admin import SingletonModelAdmin
from django.contrib import admin from django.contrib import admin
from .models import AnalyticsIdentifier, AnalyticsTokens from .models import AnalyticsIdentifier, AnalyticsTokens
@admin.register(AnalyticsIdentifier) @admin.register(AnalyticsIdentifier)
class AnalyticsIdentifierAdmin(SingletonModelAdmin): class AnalyticsIdentifierAdmin(admin.ModelAdmin):
search_fields = ['identifier', ] search_fields = ['identifier', ]
list_display = ['identifier', ] list_display = ('identifier',)
@admin.register(AnalyticsTokens) @admin.register(AnalyticsTokens)
class AnalyticsTokensAdmin(admin.ModelAdmin): class AnalyticsTokensAdmin(admin.ModelAdmin):
search_fields = ['name', ] search_fields = ['name', ]
list_display = ['name', 'type', ] list_display = ('name', 'type',)

View File

@@ -1,17 +0,0 @@
# 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'},
),
]

View File

@@ -1,21 +1,27 @@
from typing import Literal
from uuid import uuid4 from uuid import uuid4
from solo.models import SingletonModel from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class AnalyticsIdentifier(SingletonModel): class AnalyticsIdentifier(models.Model):
identifier = models.UUIDField(default=uuid4, editable=False) identifier = models.UUIDField(
default=uuid4,
editable=False)
def __str__(self) -> Literal['Analytics Identifier']: def __str__(self) -> str:
return "Analytics Identifier" 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)
class Meta:
verbose_name = "Analytics Identifier"
class AnalyticsTokens(models.Model): class AnalyticsTokens(models.Model):

View File

@@ -9,7 +9,7 @@ from django.conf import settings
from allianceauth import __version__ from allianceauth import __version__
from .models import AnalyticsIdentifier, AnalyticsTokens from .models import AnalyticsIdentifier, AnalyticsTokens
from .utils import existence_baremetal_or_docker, install_stat_addons, install_stat_tokens, install_stat_users from .utils import install_stat_addons, install_stat_tokens, install_stat_users
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -67,8 +67,8 @@ def analytics_event(namespace: str,
value=value).apply_async(priority=9) value=value).apply_async(priority=9)
@shared_task @shared_task()
def analytics_daily_stats() -> None: def analytics_daily_stats():
"""Celery Task: Do not call directly """Celery Task: Do not call directly
Gathers a series of daily statistics Gathers a series of daily statistics
@@ -77,7 +77,6 @@ def analytics_daily_stats() -> None:
users = install_stat_users() users = install_stat_users()
tokens = install_stat_tokens() tokens = install_stat_tokens()
addons = install_stat_addons() addons = install_stat_addons()
existence_type = existence_baremetal_or_docker()
logger.debug("Running Daily Analytics Upload") logger.debug("Running Daily Analytics Upload")
analytics_event(namespace='allianceauth.analytics', analytics_event(namespace='allianceauth.analytics',
@@ -85,11 +84,6 @@ def analytics_daily_stats() -> None:
label='existence', label='existence',
value=1, value=1,
event_type='Stats') 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', analytics_event(namespace='allianceauth.analytics',
task='send_install_stats', task='send_install_stats',
label='users', label='users',
@@ -105,6 +99,7 @@ def analytics_daily_stats() -> None:
label='addons', label='addons',
value=addons, value=addons,
event_type='Stats') event_type='Stats')
for appconfig in apps.get_app_configs(): for appconfig in apps.get_app_configs():
if appconfig.label in [ if appconfig.label in [
"django_celery_beat", "django_celery_beat",
@@ -140,7 +135,7 @@ def analytics_daily_stats() -> None:
event_type='Stats') event_type='Stats')
@shared_task @shared_task()
def send_ga_tracking_celery_event( def send_ga_tracking_celery_event(
measurement_id: str, measurement_id: str,
secret: str, secret: str,
@@ -170,7 +165,7 @@ def send_ga_tracking_celery_event(
} }
payload = { payload = {
'client_id': AnalyticsIdentifier.get_solo().identifier.hex, 'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex,
"user_properties": { "user_properties": {
"allianceauth_version": { "allianceauth_version": {
"value": __version__ "value": __version__

View File

@@ -1,5 +1,6 @@
from uuid import uuid4 from uuid import UUID, uuid4
from django.core.exceptions import ValidationError
from django.test.testcases import TestCase from django.test.testcases import TestCase
from allianceauth.analytics.models import AnalyticsIdentifier from allianceauth.analytics.models import AnalyticsIdentifier
@@ -12,4 +13,14 @@ uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
class TestAnalyticsIdentifier(TestCase): class TestAnalyticsIdentifier(TestCase):
def test_identifier_random(self): def test_identifier_random(self):
self.assertNotEqual(AnalyticsIdentifier.get_solo(), uuid4) 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))

View File

@@ -1,5 +1,3 @@
import os
from django.apps import apps from django.apps import apps
from esi.models import Token from esi.models import Token
@@ -38,16 +36,3 @@ def install_stat_addons() -> int:
The Number of Installed Apps""" The Number of Installed Apps"""
addons = len(list(apps.get_app_configs())) addons = len(list(apps.get_app_configs()))
return addons 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"

View File

@@ -32,7 +32,7 @@ class State(models.Model):
class Meta: class Meta:
ordering = ['-priority'] ordering = ['-priority']
def __str__(self) -> str: def __str__(self):
return self.name return self.name
def available_to_character(self, character): def available_to_character(self, character):
@@ -126,7 +126,7 @@ class UserProfile(models.Model):
self.save(update_fields=['state']) self.save(update_fields=['state'])
notify( notify(
self.user, self.user,
_(f'State changed to: {state}'), _('State changed to: {}'.format(state)),
_('Your user\'s state is now: %(state)s') _('Your user\'s state is now: %(state)s')
% ({'state': state}), % ({'state': state}),
'info' 'info'
@@ -153,7 +153,7 @@ class CharacterOwnership(models.Model):
class Meta: class Meta:
default_permissions = ('change', 'delete') default_permissions = ('change', 'delete')
ordering = ['user', 'character__character_name'] ordering = ['user', 'character__character_name']
def __str__(self) -> str: def __str__(self):
return f"{self.user}: {self.character}" return f"{self.user}: {self.character}"
@@ -166,5 +166,5 @@ class OwnershipRecord(models.Model):
class Meta: class Meta:
ordering = ['-created'] ordering = ['-created']
def __str__(self) -> str: def __str__(self):
return f"{self.user}: {self.character} on {self.created}" return f"{self.user}: {self.character} on {self.created}"

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base-bs5.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% block page_title %} {% block page_title %}
@@ -14,7 +13,7 @@
{% block content %} {% block content %}
<div> <div>
<p class="mb-3"> <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://developers.eveonline.com/authorized-apps 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://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
</p> </p>
<table class="table w-100" id="table_tokens"> <table class="table w-100" id="table_tokens">
@@ -51,23 +50,20 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include "bundles/datatables-js-bs5.html" %} {% include "bundles/datatables-js-bs5.html" %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
let grp = 2; let grp = 2;
$('#table_tokens').DataTable({ const table = $('#table_tokens').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
'columnDefs': [{orderable: false, targets: [0, 1]}, { 'columnDefs': [{orderable: false, targets: [0, 1]}, {
'visible': false, 'visible': false,
'targets': grp 'targets': grp
}], }],
'order': [[grp, 'asc']], 'order': [[grp, 'asc']],
'drawCallback': function (settings) { 'drawCallback': function (settings) {
const api = this.api(); var api = this.api();
const rows = api.rows({page: 'current'}).nodes(); var rows = api.rows({page: 'current'}).nodes();
let last = null; var last = null;
api.column(grp, {page: 'current'}) api.column(grp, {page: 'current'})
.data() .data()
.each((group, i) => { .each((group, i) => {

View File

@@ -5,11 +5,11 @@
{% csrf_token %} {% csrf_token %}
<select class="form-select" onchange="this.form.submit()" class="form-control" id="lang-select" name="language"> <select class="form-select" onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_available_languages as LANGUAGES %} {% get_language_info_list for LANGUAGES as languages %}
{% for lang_code, lang_name in LANGUAGES %} {% for language in languages %}
<option lang="{{ lang_code }}" value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}> <option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
{{ lang_code|language_name_local|capfirst }} ({{ lang_code }}) {{ language.name_local|capfirst }} ({{ language.code }})
</option> </option>
{% endfor %} {% endfor %}
</select> </select>

View File

@@ -29,7 +29,7 @@
</p> </p>
<p class="text-center"> <p class="text-center">
<a class="text-reset" href="https://developers.eveonline.com/authorized-apps" target="_blank" rel="noopener noreferrer"> <a class="text-reset" href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
{% translate "Manage ESI Applications" %} {% translate "Manage ESI Applications" %}
</a> </a>
</p> </p>

View File

@@ -1,3 +1,4 @@
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.test.testcases import TestCase from django.test.testcases import TestCase

View File

@@ -38,6 +38,7 @@ urlpatterns = [
name='token_refresh' name='token_refresh'
), ),
path('dashboard/', views.dashboard, name='dashboard'), 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('task-counts/', views.task_counts, name='task_counts'),
path('esi-check/', views.esi_check, name='esi_check'), path('esi-check/', views.esi_check, name='esi_check'),
] ]

View File

@@ -175,9 +175,9 @@ def add_character(request, token):
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter( if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
owner_hash=token.character_owner_hash).filter(user=request.user).exists(): owner_hash=token.character_owner_hash).filter(user=request.user).exists():
messages.success(request, _( messages.success(request, _(
f'Added {token.character_name} to your account.')) 'Added {name} to your account.'.format(name=token.character_name)))
else: else:
messages.error(request, _(f'Failed to add {token.character_name} to your account: they already have an account.')) messages.error(request, _('Failed to add {name} to your account: they already have an account.'.format(name=token.character_name)))
return redirect('authentication:dashboard') return redirect('authentication:dashboard')
@@ -392,3 +392,12 @@ def esi_check(request) -> JsonResponse:
"data": check_for_override_esi_error_message(_r) "data": check_for_override_esi_error_message(_r)
} }
return JsonResponse(data) 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')

View File

@@ -1,8 +1,4 @@
""" import datetime
Django system checks for Alliance Auth
"""
from datetime import datetime, timezone
from sqlite3.dbapi2 import sqlite_version_info from sqlite3.dbapi2 import sqlite_version_info
from celery import current_app from celery import current_app
@@ -11,6 +7,7 @@ from packaging.version import InvalidVersion, Version as Pep440Version
from django import db from django import db
from django.conf import settings from django.conf import settings
from django.core.checks import CheckMessage, Error, Warning, register from django.core.checks import CheckMessage, Error, Warning, register
from django.utils import timezone
from allianceauth.utils.cache import get_redis_client from allianceauth.utils.cache import get_redis_client
@@ -22,415 +19,141 @@ B = Configuration
@register() @register()
def django_settings(app_configs, **kwargs) -> list[CheckMessage]: def django_settings(app_configs, **kwargs) -> list[CheckMessage]:
"""
Check that Django settings are correctly configured
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
errors: list[CheckMessage] = [] errors: list[CheckMessage] = []
# Check for SITE_URL
if hasattr(settings, "SITE_URL"): if hasattr(settings, "SITE_URL"):
# Check if SITE_URL is empty if settings.SITE_URL[-1] == "/":
if settings.SITE_URL == "": errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005"))
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: else:
errors.append( 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"))
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"): 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: if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
errors.append( 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"))
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: else:
errors.append( 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"))
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 return errors
@register() @register()
def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]: 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] = [] errors: list[CheckMessage] = []
try: try:
redis_version = Pep440Version(get_redis_client().info()["redis_version"]) redis_version = Pep440Version(get_redis_client().info()['redis_version'])
except InvalidVersion: except InvalidVersion:
errors.append(Warning("Unable to confirm Redis Version")) errors.append(Warning("Unable to confirm Redis Version"))
return errors return errors
if ( 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):
redis_version.major == 7 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"))
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: elif redis_version.major == 7 and redis_version.minor == 0:
errors.append( 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"))
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: elif redis_version.major == 6 and redis_version.minor == 2:
errors.append( 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"))
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]: elif redis_version.major in [6, 5]:
errors.append( 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"))
Error(
msg=f"Redis {redis_version.public} EOL",
hint=allianceauth_redis_install_link,
id="allianceauth.checks.A003",
)
)
return errors return errors
@register() @register()
def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]: 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] = [] errors: list[CheckMessage] = []
for connection in db.connections.all(): for connection in db.connections.all():
if connection.vendor == "mysql": if connection.vendor == "mysql":
try: try:
mysql_version = Pep440Version( mysql_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
".".join(str(i) for i in connection.mysql_version)
)
except InvalidVersion: except InvalidVersion:
errors.append(Warning("Unable to confirm MySQL Version")) errors.append(Warning("Unable to confirm MySQL Version"))
return errors return errors
# MySQL 8 # MySQL 8
if mysql_version.major == 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):
if mysql_version.minor == 4 and datetime.now(timezone.utc) > datetime( 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"))
year=2032, month=4, day=30, tzinfo=timezone.utc 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"))
errors.append( elif mysql_version.major == 8 and mysql_version.minor == 2:
Error( 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"))
msg=f"MySQL {mysql_version.public} EOL", elif mysql_version.major == 8 and mysql_version.minor == 1:
hint=mysql_quick_guide_link, 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"))
id="allianceauth.checks.A004", 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
# Demote versions down here once EOL 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"))
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 return errors
@register() @register()
def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: # noqa: C901 def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]:
"""
Check that MariaDB is a supported version
"""
mariadb_download_link = "https://mariadb.org/download/?t=repo-config"
errors: list[CheckMessage] = [] errors: list[CheckMessage] = []
for connection in db.connections.all(): for connection in db.connections.all():
# TODO: Find a way to determine MySQL vs. MariaDB if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB
if connection.vendor == "mysql":
try: try:
mariadb_version = Pep440Version( mariadb_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
".".join(str(i) for i in connection.mysql_version)
)
except InvalidVersion: except InvalidVersion:
errors.append(Warning("Unable to confirm MariaDB Version")) errors.append(Warning("Unable to confirm MariaDB Version"))
return errors return errors
# MariaDB 11 # MariaDB 11
if mariadb_version.major == 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):
if mariadb_version.minor == 4 and datetime.now(timezone.utc) > datetime( errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010"))
year=2029, month=5, day=19, tzinfo=timezone.utc 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"))
errors.append( if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=datetime.timezone.utc):
Error( errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011"))
msg=f"MariaDB {mariadb_version.public} EOL", elif mariadb_version.major == 11 and mariadb_version.minor == 1:
hint=mariadb_download_link, errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019"))
id="allianceauth.checks.A010", 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"))
# 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 # MariaDB 10
elif mariadb_version.major == 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):
if mariadb_version.minor == 11 and datetime.now(timezone.utc) > datetime( errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014"))
year=2028, month=2, day=10, tzinfo=timezone.utc 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"))
errors.append( 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):
Error( errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016"))
msg=f"MariaDB {mariadb_version.public} EOL", elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL
hint=mariadb_download_link, errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017"))
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 return errors
@register() @register()
def system_package_sqlite(app_configs, **kwargs) -> list[CheckMessage]: 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] = [] errors: list[CheckMessage] = []
for connection in db.connections.all(): for connection in db.connections.all():
if connection.vendor == "sqlite": if connection.vendor == "sqlite":
try: try:
sqlite_version = Pep440Version( sqlite_version = Pep440Version(".".join(str(i) for i in sqlite_version_info))
".".join(str(i) for i in sqlite_version_info)
)
except InvalidVersion: except InvalidVersion:
errors.append(Warning("Unable to confirm SQLite Version")) errors.append(Warning("Unable to confirm SQLite Version"))
return errors return errors
if sqlite_version.major == 3 and sqlite_version.minor < 27: if sqlite_version.major == 3 and sqlite_version.minor < 27:
errors.append( errors.append(Error(f"SQLite {sqlite_version.public} Unsupported by Django", hint="https://pkgs.org/download/sqlite3", id="allianceauth.checks.A020"))
Error(
msg=f"SQLite {sqlite_version.public} Unsupported by Django",
hint="https://pkgs.org/download/sqlite3",
id="allianceauth.checks.A020",
)
)
return errors return errors
@register() @register()
def sql_settings(app_configs, **kwargs) -> list[CheckMessage]: 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] = [] errors: list[CheckMessage] = []
for connection in db.connections.all(): for connection in db.connections.all():
if connection.vendor == "mysql": if connection.vendor == "mysql":
try: try:
if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4": if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4":
errors.append( 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"))
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: except KeyError:
errors.append( 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"))
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 # This hasn't actually been set on AA yet
# try: # try:
# if ( # if connection.settings_dict["OPTIONS"]["collation"] != "utf8mb4_unicode_ci":
# connection.settings_dict["OPTIONS"]["collation"] # 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"))
# != "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: # except KeyError:
# errors.append( # 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"))
# 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": # if connection.vendor == "sqlite":
@@ -439,57 +162,19 @@ def sql_settings(app_configs, **kwargs) -> list[CheckMessage]:
@register() @register()
def celery_settings(app_configs, **kwargs) -> list[CheckMessage]: 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] = [] errors: list[CheckMessage] = []
try: try:
if current_app.conf.broker_transport_options != { if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}:
"priority_steps": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], errors.append(Error("Celery Priorities are not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
"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: except KeyError:
errors.append( errors.append(Error("Celery Priorities are not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
Error(
msg="Celery Priorities are not set",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
id="allianceauth.checks.B003",
)
)
try: try:
if not current_app.conf.broker_connection_retry_on_startup: if current_app.conf.broker_connection_retry_on_startup is not True:
errors.append( errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
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: except KeyError:
errors.append( errors.append(Error("Celery broker_connection_retry_on_startup not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
Error(
msg="Celery broker_connection_retry_on_startup not set",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
id="allianceauth.checks.B004",
)
)
return errors return errors

View File

@@ -70,7 +70,7 @@ def forward(apps, schema_editor):
perm.delete() perm.delete()
def reverse(apps, schema_editor): # noqa: C901 def reverse(apps, schema_editor):
perm_dict = user_permissions_dict(apps) perm_dict = user_permissions_dict(apps)
corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats']) corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])

View File

@@ -45,7 +45,7 @@ class CorpStats(models.Model):
def __str__(self) -> str: def __str__(self):
return f"{self.__class__.__name__} for {self.corp}" return f"{self.__class__.__name__} for {self.corp}"
def update(self): def update(self):
@@ -154,7 +154,7 @@ class CorpMember(models.Model):
unique_together = ('corpstats', 'character_id') unique_together = ('corpstats', 'character_id')
ordering = ['character_name'] ordering = ['character_name']
def __str__(self) -> str: def __str__(self):
return self.character_name return self.character_name
@property @property

View File

@@ -1,6 +1,5 @@
{% extends 'corputils/base.html' %} {% extends 'corputils/base.html' %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
@@ -239,12 +238,9 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %} {% include 'bundles/datatables-js-bs5.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
$('#table-mains').DataTable({ $('#table-mains').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [ "columnDefs": [
{ "sortable": false, "targets": [1] }, { "sortable": false, "targets": [1] },
], ],
@@ -253,7 +249,6 @@
}); });
$('#table-members').DataTable({ $('#table-members').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [ "columnDefs": [
{ "searchable": false, "targets": [0, 2] }, { "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] }, { "sortable": false, "targets": [0, 2] },
@@ -264,7 +259,6 @@
}); });
$('#table-unregistered').DataTable({ $('#table-unregistered').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [ "columnDefs": [
{ "searchable": false, "targets": [0, 2] }, { "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] }, { "sortable": false, "targets": [0, 2] },

View File

@@ -1,6 +1,5 @@
{% extends "corputils/base.html" %} {% extends "corputils/base.html" %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% block member_data %} {% block member_data %}
@@ -43,12 +42,9 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %} {% include 'bundles/datatables-js-bs5.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
$('#table-search').DataTable({ $('#table-search').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"stateSave": true, "stateSave": true,
"stateDuration": 0 "stateDuration": 0
}); });

View File

@@ -62,7 +62,7 @@ def corpstats_add(request, token):
@login_required @login_required
@user_passes_test(access_corpstats_test) @user_passes_test(access_corpstats_test)
def corpstats_view(request, corp_id=None): # noqa: C901 def corpstats_view(request, corp_id=None):
corpstats = None corpstats = None
# get requested model # get requested model

View File

@@ -1,3 +0,0 @@
"""
Alliance Auth Crontab Utilities
"""

View File

@@ -1,14 +0,0 @@
"""
Crontab App Config
"""
from django.apps import AppConfig
class CrontabConfig(AppConfig):
"""
Crontab App Config
"""
name = "allianceauth.crontab"
label = "crontab"

View File

@@ -1,30 +0,0 @@
# 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',
},
),
]

View File

@@ -1,25 +0,0 @@
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"

View File

@@ -1,68 +0,0 @@
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)

View File

@@ -1,64 +0,0 @@
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")

View File

@@ -1,82 +0,0 @@
# 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)

View File

@@ -1,52 +0,0 @@
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

View File

@@ -49,8 +49,8 @@ class EveFactionForm(EveEntityForm):
def clean_id(self): def clean_id(self):
try: try:
assert self.Meta.model.provider.get_faction(self.cleaned_data['id']) assert self.Meta.model.provider.get_faction(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound) as e: except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError('faction', self.cleaned_data['id']) from e raise EveEntityNotFoundError('faction', self.cleaned_data['id'])
if self.Meta.model.objects.filter(faction_id=self.cleaned_data['id']).exists(): if self.Meta.model.objects.filter(faction_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError('faction', self.cleaned_data['id']) raise EveEntityExistsError('faction', self.cleaned_data['id'])
return self.cleaned_data['id'] return self.cleaned_data['id']
@@ -70,8 +70,8 @@ class EveCharacterForm(EveEntityForm):
def clean_id(self): def clean_id(self):
try: try:
assert self.Meta.model.provider.get_character(self.cleaned_data['id']) assert self.Meta.model.provider.get_character(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound) as e: except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
if self.Meta.model.objects.filter(character_id=self.cleaned_data['id']).exists(): if self.Meta.model.objects.filter(character_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id']) raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
return self.cleaned_data['id'] return self.cleaned_data['id']
@@ -90,8 +90,8 @@ class EveCorporationForm(EveEntityForm):
def clean_id(self): def clean_id(self):
try: try:
assert self.Meta.model.provider.get_corporation(self.cleaned_data['id']) assert self.Meta.model.provider.get_corporation(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound) as e: except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
if self.Meta.model.objects.filter(corporation_id=self.cleaned_data['id']).exists(): if self.Meta.model.objects.filter(corporation_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id']) raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
return self.cleaned_data['id'] return self.cleaned_data['id']
@@ -110,8 +110,8 @@ class EveAllianceForm(EveEntityForm):
def clean_id(self): def clean_id(self):
try: try:
assert self.Meta.model.provider.get_alliance(self.cleaned_data['id']) assert self.Meta.model.provider.get_alliance(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound) as e: except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
if self.Meta.model.objects.filter(alliance_id=self.cleaned_data['id']).exists(): if self.Meta.model.objects.filter(alliance_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id']) raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
return self.cleaned_data['id'] return self.cleaned_data['id']

View File

@@ -81,7 +81,7 @@ class AutogroupsConfig(models.Model):
objects = AutogroupsConfigManager() objects = AutogroupsConfigManager()
def __str__(self) -> str: def __str__(self):
return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None)) return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -235,7 +235,7 @@ class ManagedGroup(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def __str__(self) -> str: def __str__(self):
return f"Managed Group: {self.group.name}" return f"Managed Group: {self.group.name}"
class ManagedCorpGroup(ManagedGroup): class ManagedCorpGroup(ManagedGroup):

View File

@@ -4,7 +4,7 @@ from urllib.parse import quote, urljoin
from . import _ESI_CATEGORY_ALLIANCE, _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_REGION, _ESI_CATEGORY_SOLARSYSTEM from . import _ESI_CATEGORY_ALLIANCE, _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_REGION, _ESI_CATEGORY_SOLARSYSTEM
_BASE_URL = 'https://evemaps.dotlan.net' _BASE_URL = 'http://evemaps.dotlan.net'
def _build_url(category: str, name: str) -> str: def _build_url(category: str, name: str) -> str:

View File

@@ -29,29 +29,29 @@ class TestDotlan(TestCase):
def test_alliance_url(self): def test_alliance_url(self):
self.assertEqual( self.assertEqual(
dotlan.alliance_url('Wayne Enterprices'), dotlan.alliance_url('Wayne Enterprices'),
'https://evemaps.dotlan.net/alliance/Wayne_Enterprices' 'http://evemaps.dotlan.net/alliance/Wayne_Enterprices'
) )
def test_corporation_url(self): def test_corporation_url(self):
self.assertEqual( self.assertEqual(
dotlan.corporation_url('Wayne Technology'), dotlan.corporation_url('Wayne Technology'),
'https://evemaps.dotlan.net/corp/Wayne_Technology' 'http://evemaps.dotlan.net/corp/Wayne_Technology'
) )
self.assertEqual( self.assertEqual(
dotlan.corporation_url('Crédit Agricole'), dotlan.corporation_url('Crédit Agricole'),
'https://evemaps.dotlan.net/corp/Cr%C3%A9dit_Agricole' 'http://evemaps.dotlan.net/corp/Cr%C3%A9dit_Agricole'
) )
def test_region_url(self): def test_region_url(self):
self.assertEqual( self.assertEqual(
dotlan.region_url('Black Rise'), dotlan.region_url('Black Rise'),
'https://evemaps.dotlan.net/map/Black_Rise' 'http://evemaps.dotlan.net/map/Black_Rise'
) )
def test_solar_system_url(self): def test_solar_system_url(self):
self.assertEqual( self.assertEqual(
dotlan.solar_system_url('Jita'), dotlan.solar_system_url('Jita'),
'https://evemaps.dotlan.net/system/Jita' 'http://evemaps.dotlan.net/system/Jita'
) )

View File

@@ -32,7 +32,7 @@ class EveFactionInfo(models.Model):
provider = providers.provider provider = providers.provider
def __str__(self) -> str: def __str__(self):
return self.faction_name return self.faction_name
@staticmethod @staticmethod
@@ -80,7 +80,7 @@ class EveAllianceInfo(models.Model):
class Meta: class Meta:
indexes = [models.Index(fields=['executor_corp_id',])] indexes = [models.Index(fields=['executor_corp_id',])]
def __str__(self) -> str: def __str__(self):
return self.alliance_name return self.alliance_name
def populate_alliance(self): def populate_alliance(self):
alliance = self.provider.get_alliance(self.alliance_id) alliance = self.provider.get_alliance(self.alliance_id)
@@ -152,7 +152,7 @@ class EveCorporationInfo(models.Model):
class Meta: class Meta:
indexes = [models.Index(fields=['ceo_id',]),] indexes = [models.Index(fields=['ceo_id',]),]
def __str__(self) -> str: def __str__(self):
return self.corporation_name return self.corporation_name
def update_corporation(self, corp: providers.Corporation = None): def update_corporation(self, corp: providers.Corporation = None):
if corp is None: if corp is None:
@@ -226,7 +226,7 @@ class EveCharacter(models.Model):
models.Index(fields=['faction_id',]), models.Index(fields=['faction_id',]),
] ]
def __str__(self) -> str: def __str__(self):
return self.character_name return self.character_name
@property @property

View File

@@ -1,7 +1,6 @@
import logging import logging
import os import os
from bravado.client import SwaggerClient
from bravado.exception import HTTPError, HTTPNotFound, HTTPUnprocessableEntity from bravado.exception import HTTPError, HTTPNotFound, HTTPUnprocessableEntity
from jsonschema.exceptions import RefResolutionError from jsonschema.exceptions import RefResolutionError
@@ -9,7 +8,7 @@ from django.conf import settings
from esi.clients import esi_client_factory from esi.clients import esi_client_factory
from allianceauth import __version__, __title__, __url__ from allianceauth import __version__
from allianceauth.utils.django import StartupCommand from allianceauth.utils.django import StartupCommand
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname( SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
@@ -37,7 +36,7 @@ class ObjectNotFound(Exception):
self.id = obj_id self.id = obj_id
self.type = type_name self.type = type_name
def __str__(self) -> str: def __str__(self):
return f'{self.type} with ID {self.id} not found.' return f'{self.type} with ID {self.id} not found.'
@@ -47,7 +46,7 @@ class Entity:
self.id = id self.id = id
self.name = name self.name = name
def __str__(self) -> str: def __str__(self):
return self.name return self.name
def __repr__(self): def __repr__(self):
@@ -176,11 +175,7 @@ class EveProvider:
class EveSwaggerProvider(EveProvider): class EveSwaggerProvider(EveProvider):
def __init__(self, token=None, adapter=None) -> None: def __init__(self, token=None, adapter=None):
self._token = token
self.adapter = adapter or self
self._faction_list = None # what are the odds this will change? could cache forever!
if settings.DEBUG or StartupCommand().is_management_command: if settings.DEBUG or StartupCommand().is_management_command:
self._client = None self._client = None
logger.info('ESI client will be loaded on-demand') logger.info('ESI client will be loaded on-demand')
@@ -188,10 +183,9 @@ class EveSwaggerProvider(EveProvider):
logger.info('Loading ESI client') logger.info('Loading ESI client')
try: try:
self._client = esi_client_factory( self._client = esi_client_factory(
token=self._token, token=token,
ua_appname=__title__, spec_file=SWAGGER_SPEC_PATH,
ua_version=__version__, app_info_text=f"allianceauth v{__version__}"
ua_url=__url__,
) )
except (HTTPError, RefResolutionError): except (HTTPError, RefResolutionError):
logger.exception( logger.exception(
@@ -200,18 +194,19 @@ class EveSwaggerProvider(EveProvider):
) )
self._client = None self._client = None
self._token = token
self.adapter = adapter or self
self._faction_list = None # what are the odds this will change? could cache forever!
@property @property
def client(self) -> SwaggerClient: def client(self):
if self._client is None: if self._client is None:
self._client = esi_client_factory( self._client = esi_client_factory(
token=self._token, token=self._token, spec_file=SWAGGER_SPEC_PATH, app_info_text=("allianceauth v" + __version__)
ua_appname=__title__,
ua_version=__version__,
ua_url=__url__,
) )
return self._client return self._client
def __str__(self) -> str: def __str__(self):
return 'esi' return 'esi'
def get_alliance(self, alliance_id: int) -> Alliance: def get_alliance(self, alliance_id: int) -> Alliance:
@@ -228,8 +223,8 @@ class EveSwaggerProvider(EveProvider):
faction_id=data['faction_id'] if 'faction_id' in data else None, faction_id=data['faction_id'] if 'faction_id' in data else None,
) )
return model return model
except HTTPNotFound as e: except HTTPNotFound:
raise ObjectNotFound(alliance_id, 'alliance') from e raise ObjectNotFound(alliance_id, 'alliance')
def get_corp(self, corp_id: int) -> Corporation: def get_corp(self, corp_id: int) -> Corporation:
"""Fetch corporation from ESI.""" """Fetch corporation from ESI."""
@@ -245,8 +240,8 @@ class EveSwaggerProvider(EveProvider):
faction_id=data['faction_id'] if 'faction_id' in data else None, faction_id=data['faction_id'] if 'faction_id' in data else None,
) )
return model return model
except HTTPNotFound as e: except HTTPNotFound:
raise ObjectNotFound(corp_id, 'corporation') from e raise ObjectNotFound(corp_id, 'corporation')
def get_character(self, character_id: int) -> Character: def get_character(self, character_id: int) -> Character:
"""Fetch character from ESI.""" """Fetch character from ESI."""
@@ -261,8 +256,8 @@ class EveSwaggerProvider(EveProvider):
faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None, faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None,
) )
return model return model
except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound) as e: except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound):
raise ObjectNotFound(character_id, 'character') from e raise ObjectNotFound(character_id, 'character')
def _fetch_character_name(self, character_id: int) -> str: def _fetch_character_name(self, character_id: int) -> str:
"""Fetch character name from ESI.""" """Fetch character name from ESI."""
@@ -293,16 +288,16 @@ class EveSwaggerProvider(EveProvider):
return Entity(id=f['faction_id'], name=f['name']) return Entity(id=f['faction_id'], name=f['name'])
else: else:
raise KeyError() raise KeyError()
except (HTTPNotFound, HTTPUnprocessableEntity, KeyError) as e: except (HTTPNotFound, HTTPUnprocessableEntity, KeyError):
raise ObjectNotFound(faction_id, 'faction') from e raise ObjectNotFound(faction_id, 'faction')
def get_itemtype(self, type_id: int) -> ItemType: def get_itemtype(self, type_id: int) -> ItemType:
"""Fetch inventory item from ESI.""" """Fetch inventory item from ESI."""
try: try:
data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result() data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result()
return ItemType(id=type_id, name=data['name']) return ItemType(id=type_id, name=data['name'])
except (HTTPNotFound, HTTPUnprocessableEntity) as e: except (HTTPNotFound, HTTPUnprocessableEntity):
raise ObjectNotFound(type_id, 'type') from e raise ObjectNotFound(type_id, 'type')
provider = EveSwaggerProvider() provider = EveSwaggerProvider()

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,4 @@
import logging import logging
from random import randint
from celery import shared_task from celery import shared_task
@@ -9,8 +8,7 @@ from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TASK_PRIORITY = 7 TASK_PRIORITY = 7
CHARACTER_AFFILIATION_CHUNK_SIZE = 500 CHUNK_SIZE = 500
EVEONLINE_TASK_JITTER = 600
def chunks(lst, n): def chunks(lst, n):
@@ -20,13 +18,13 @@ def chunks(lst, n):
@shared_task @shared_task
def update_corp(corp_id: int) -> None: def update_corp(corp_id):
"""Update given corporation from ESI""" """Update given corporation from ESI"""
EveCorporationInfo.objects.update_corporation(corp_id) EveCorporationInfo.objects.update_corporation(corp_id)
@shared_task @shared_task
def update_alliance(alliance_id: int) -> None: def update_alliance(alliance_id):
"""Update given alliance from ESI""" """Update given alliance from ESI"""
EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance() EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance()
@@ -38,30 +36,23 @@ def update_character(character_id: int) -> None:
@shared_task @shared_task
def run_model_update() -> None: def run_model_update():
"""Update all alliances, corporations and characters from ESI""" """Update all alliances, corporations and characters from ESI"""
# Queue update tasks for Known Corporation Models #update existing corp models
for corp in EveCorporationInfo.objects.all().values('corporation_id'): for corp in EveCorporationInfo.objects.all().values('corporation_id'):
update_corp.apply_async( update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
args=[corp['corporation_id']],
priority=TASK_PRIORITY,
countdown=randint(1, EVEONLINE_TASK_JITTER))
# Queue update tasks for Known Alliance Models # update existing alliance models
for alliance in EveAllianceInfo.objects.all().values('alliance_id'): for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
update_alliance.apply_async( update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
args=[alliance['alliance_id']],
priority=TASK_PRIORITY,
countdown=randint(1, EVEONLINE_TASK_JITTER))
# Queue update tasks for Known Character Models # update existing character models
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True) character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
for character_ids_chunk in chunks(character_ids, CHARACTER_AFFILIATION_CHUNK_SIZE): for character_ids_chunk in chunks(character_ids, CHUNK_SIZE):
update_character_chunk.apply_async( update_character_chunk.apply_async(
args=[character_ids_chunk], args=[character_ids_chunk], priority=TASK_PRIORITY
priority=TASK_PRIORITY, )
countdown=randint(1, EVEONLINE_TASK_JITTER))
@shared_task @shared_task
@@ -76,9 +67,8 @@ def update_character_chunk(character_ids_chunk: list):
logger.info("Failed to bulk update characters. Attempting single updates") logger.info("Failed to bulk update characters. Attempting single updates")
for character_id in character_ids_chunk: for character_id in character_ids_chunk:
update_character.apply_async( update_character.apply_async(
args=[character_id], args=[character_id], priority=TASK_PRIORITY
priority=TASK_PRIORITY, )
countdown=randint(1, EVEONLINE_TASK_JITTER))
return return
affiliations = { affiliations = {
@@ -116,5 +106,5 @@ def update_character_chunk(character_ids_chunk: list):
if corp_changed or alliance_changed or name_changed: if corp_changed or alliance_changed or name_changed:
update_character.apply_async( update_character.apply_async(
args=[character.get('character_id')], args=[character.get('character_id')], priority=TASK_PRIORITY
priority=TASK_PRIORITY) )

View File

@@ -13,7 +13,7 @@ class BravadoResponseStub:
self.headers = headers if headers else {} self.headers = headers if headers else {}
self.raw_bytes = raw_bytes self.raw_bytes = raw_bytes
def __str__(self) -> str: def __str__(self):
return f"{self.status_code} {self.reason}" return f"{self.status_code} {self.reason}"
@@ -57,11 +57,11 @@ class EsiClientStub:
} }
try: try:
return BravadoOperationStub(data[int(alliance_id)]) return BravadoOperationStub(data[int(alliance_id)])
except KeyError as e: except KeyError:
response = BravadoResponseStub( response = BravadoResponseStub(
404, f"Alliance with ID {alliance_id} not found" 404, f"Alliance with ID {alliance_id} not found"
) )
raise HTTPNotFound(response) from e raise HTTPNotFound(response)
@staticmethod @staticmethod
def get_alliances_alliance_id_corporations(alliance_id): def get_alliances_alliance_id_corporations(alliance_id):
@@ -87,11 +87,11 @@ class EsiClientStub:
} }
try: try:
return BravadoOperationStub(data[int(character_id)]) return BravadoOperationStub(data[int(character_id)])
except KeyError as e: except KeyError:
response = BravadoResponseStub( response = BravadoResponseStub(
404, f"Character with ID {character_id} not found" 404, f"Character with ID {character_id} not found"
) )
raise HTTPNotFound(response) from e raise HTTPNotFound(response)
@staticmethod @staticmethod
def post_characters_affiliation(characters: list): def post_characters_affiliation(characters: list):
@@ -147,11 +147,11 @@ class EsiClientStub:
} }
try: try:
return BravadoOperationStub(data[int(corporation_id)]) return BravadoOperationStub(data[int(corporation_id)])
except KeyError as e: except KeyError:
response = BravadoResponseStub( response = BravadoResponseStub(
404, f"Corporation with ID {corporation_id} not found" 404, f"Corporation with ID {corporation_id} not found"
) )
raise HTTPNotFound(response) from e raise HTTPNotFound(response)
class Universe: class Universe:
@staticmethod @staticmethod

View File

@@ -676,6 +676,16 @@ class TestEveSwaggerProvider(TestCase):
self.assertTrue(mock_esi_client_factory.called) self.assertTrue(mock_esi_client_factory.called)
self.assertIsNotNone(my_provider._client) self.assertIsNotNone(my_provider._client)
@patch(MODULE_PATH + '.SWAGGER_SPEC_PATH', SWAGGER_OLD_SPEC_PATH)
@patch(MODULE_PATH + '.settings.DEBUG', False)
@patch('socket.socket')
def test_create_client_on_normal_startup_w_old_swagger_spec(
self, mock_socket
):
mock_socket.side_effect = Exception('Network blocked for testing')
my_provider = EveSwaggerProvider()
self.assertIsNone(my_provider._client)
@patch(MODULE_PATH + '.settings.DEBUG', True) @patch(MODULE_PATH + '.settings.DEBUG', True)
@patch(MODULE_PATH + '.esi_client_factory') @patch(MODULE_PATH + '.esi_client_factory')
def test_dont_create_client_on_debug_startup(self, mock_esi_client_factory): def test_dont_create_client_on_debug_startup(self, mock_esi_client_factory):
@@ -712,6 +722,6 @@ class TestEveSwaggerProvider(TestCase):
my_provider = EveSwaggerProvider() my_provider = EveSwaggerProvider()
my_client = my_provider.client my_client = my_provider.client
operation = my_client.Universe.get_universe_factions() operation = my_client.Universe.get_universe_factions()
self.assertIn( self.assertEqual(
'AllianceAuth/1.0.0 (dummy@example.net; +https://gitlab.com/allianceauth/allianceauth)', operation.future.request.headers['User-Agent'] operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0'
) )

View File

@@ -84,7 +84,7 @@ class TestUpdateTasks(TestCase):
@override_settings(CELERY_ALWAYS_EAGER=True) @override_settings(CELERY_ALWAYS_EAGER=True)
@patch('allianceauth.eveonline.providers.esi_client_factory') @patch('allianceauth.eveonline.providers.esi_client_factory')
@patch('allianceauth.eveonline.tasks.providers') @patch('allianceauth.eveonline.tasks.providers')
@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_CHUNK_SIZE', 2) @patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
class TestRunModelUpdate(TransactionTestCase): class TestRunModelUpdate(TransactionTestCase):
def test_should_run_updates(self, mock_providers, mock_esi_client_factory): def test_should_run_updates(self, mock_providers, mock_esi_client_factory):
# given # given
@@ -139,7 +139,7 @@ class TestRunModelUpdate(TransactionTestCase):
@patch('allianceauth.eveonline.tasks.update_character', wraps=update_character) @patch('allianceauth.eveonline.tasks.update_character', wraps=update_character)
@patch('allianceauth.eveonline.providers.esi_client_factory') @patch('allianceauth.eveonline.providers.esi_client_factory')
@patch('allianceauth.eveonline.tasks.providers') @patch('allianceauth.eveonline.tasks.providers')
@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_CHUNK_SIZE', 2) @patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
class TestUpdateCharacterChunk(TestCase): class TestUpdateCharacterChunk(TestCase):
@staticmethod @staticmethod
def _updated_character_ids(spy_update_character) -> set: def _updated_character_ids(spy_update_character) -> set:

View File

@@ -1,6 +1,6 @@
# Generated by Django 1.10.1 on 2016-09-05 22:20 # Generated by Django 1.10.1 on 2016-09-05 22:20
from datetime import datetime, timezone import datetime
from django.db import migrations, models from django.db import migrations, models
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='fatlink', model_name='fatlink',
name='fatdatetime', name='fatdatetime',
field=models.DateTimeField(default=datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=timezone.utc)), field=models.DateTimeField(default=datetime.datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=datetime.timezone.utc)),
), ),
] ]

View File

@@ -13,7 +13,7 @@ class Fatlink(models.Model):
hash = models.CharField(max_length=254, unique=True) hash = models.CharField(max_length=254, unique=True)
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user)) creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
def __str__(self) -> str: def __str__(self):
return self.fleet return self.fleet
@@ -28,5 +28,5 @@ class Fat(models.Model):
class Meta: class Meta:
unique_together = (('character', 'fatlink'),) unique_together = (('character', 'fatlink'),)
def __str__(self) -> str: def __str__(self):
return f"Fat-link for {self.character.character_name}" return f"Fat-link for {self.character.character_name}"

View File

@@ -1,12 +1,13 @@
import datetime
import logging import logging
import os import os
from datetime import datetime, timedelta, timezone
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.shortcuts import get_object_or_404, redirect, render 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.crypto import get_random_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -134,7 +135,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) 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} context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, 'previous_month': start_of_previous_month, 'corpid': corpid}
if datetime.datetime.now(timezone.utc) > start_of_next_month: if datetime.datetime.now() > start_of_next_month:
context.update({'next_month': start_of_next_month}) context.update({'next_month': start_of_next_month})
return render(request, 'fleetactivitytracking/fatlinkstatisticscorpview.html', context=context) return render(request, 'fleetactivitytracking/fatlinkstatisticscorpview.html', context=context)
@@ -142,12 +143,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
@login_required @login_required
@permission_required('auth.fleetactivitytracking_statistics') @permission_required('auth.fleetactivitytracking_statistics')
def fatlink_statistics_view(request, year=None, month=None): def fatlink_statistics_view(request, year=datetime.date.today().year, month=datetime.date.today().month):
if year is None:
year = datetime.date.today().year
if month is None:
month = datetime.date.today().month
year = int(year) year = int(year)
month = int(month) month = int(month)
start_of_month = datetime.datetime(year, month, 1) start_of_month = datetime.datetime(year, month, 1)
@@ -173,19 +169,16 @@ def fatlink_statistics_view(request, year=None, month=None):
stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True) 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} context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, 'previous_month': start_of_previous_month}
if datetime.datetime.now(timezone.utc) > start_of_next_month: if datetime.datetime.now() > start_of_next_month:
context.update({'next_month': start_of_next_month}) context.update({'next_month': start_of_next_month})
return render(request, 'fleetactivitytracking/fatlinkstatisticsview.html', context=context) return render(request, 'fleetactivitytracking/fatlinkstatisticsview.html', context=context)
@login_required @login_required
def fatlink_personal_statistics_view(request, year=None): def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
if year is None:
year = datetime.date.today().year
year = int(year) year = int(year)
logger.debug(f"Personal statistics view for year {year} called by {request.user}") logger.debug("Personal statistics view for year %i called by %s" % (year, request.user))
user = request.user user = request.user
logger.debug(f"fatlink_personal_statistics_view called by user {request.user}") logger.debug(f"fatlink_personal_statistics_view called by user {request.user}")
@@ -201,7 +194,7 @@ def fatlink_personal_statistics_view(request, year=None):
monthlystats = [(i + 1, datetime.date(year, i + 1, 1).strftime("%h"), monthlystats[i]) for i in range(12)] monthlystats = [(i + 1, datetime.date(year, i + 1, 1).strftime("%h"), monthlystats[i]) for i in range(12)]
if datetime.datetime.now(timezone.utc) > datetime.datetime(year + 1, 1, 1): if datetime.datetime.now() > datetime.datetime(year + 1, 1, 1):
context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1, 'next_year': year + 1} context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1, 'next_year': year + 1}
else: else:
context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1} context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1}
@@ -271,7 +264,7 @@ def click_fatlink_view(request, token, fat_hash=None):
if character_online["online"] is True: if character_online["online"] is True:
fatlink = get_object_or_404(Fatlink, hash=fat_hash) fatlink = get_object_or_404(Fatlink, hash=fat_hash)
if (datetime.now(timezone.utc) - fatlink.fatdatetime) < timedelta(seconds=(fatlink.duration * 60)): if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
if character: if character:
# get data # get data
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result() location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()

View File

@@ -13,15 +13,6 @@
} }
} }
/* Image overflow fix
------------------------------------------------------------------------------------- */
@media all {
img {
max-width: 100%;
height: auto;
}
}
/* Side Navigation /* Side Navigation
------------------------------------------------------------------------------------- */ ------------------------------------------------------------------------------------- */
@media all { @media all {
@@ -49,14 +40,14 @@
} }
/* Chevron icons */ /* Chevron icons */
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-down, #sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-down,
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-right { #sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-right {
display: block; display: block;
width: 16px; width: 16px;
} }
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-right, #sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-right,
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-down { #sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-down {
display: none; display: none;
} }
} }

View File

@@ -49,7 +49,7 @@ class GroupsMenuItem(MenuItemHook):
MenuItemHook.__init__( MenuItemHook.__init__(
self, self,
text=_("Groups"), text=_("Groups"),
classes="fa-solid fa-users", classes="fa-solid fa-user",
url_name="groupmanagement:groups", url_name="groupmanagement:groups",
order=25, order=25,
navactive=[ navactive=[

View File

@@ -1,3 +1,5 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
@@ -15,7 +17,7 @@ class GroupRequest(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
def __str__(self) -> str: def __str__(self):
return self.user.username + ":" + self.group.name return self.user.username + ":" + self.group.name
@property @property
@@ -50,7 +52,7 @@ class RequestLog(models.Model):
request_actor = models.ForeignKey(User, on_delete=models.CASCADE) request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str: def __str__(self):
return self.pk return self.pk
def requestor(self): def requestor(self):
@@ -179,7 +181,7 @@ class AuthGroup(models.Model):
) )
default_permissions = () default_permissions = ()
def __str__(self) -> str: def __str__(self):
return self.group.name return self.group.name
def group_request_approvers(self) -> set[User]: def group_request_approvers(self) -> set[User]:

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base-bs5.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load aa_i18n %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load navactive %} {% load navactive %}
@@ -74,8 +73,6 @@
{% include 'bundles/moment-js.html' with locale=True %} {% include 'bundles/moment-js.html' with locale=True %}
{% include 'bundles/filterdropdown-js.html' %} {% include 'bundles/filterdropdown-js.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$.fn.dataTable.moment = (format, locale) => { $.fn.dataTable.moment = (format, locale) => {
const types = $.fn.dataTable.ext.type; const types = $.fn.dataTable.ext.type;
@@ -97,7 +94,6 @@
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm'); $.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
$('#log-entries').DataTable({ $('#log-entries').DataTable({
language: {url: '{{ DT_LANG_PATH }}'},
order: [[0, 'desc'], [1, 'asc']], order: [[0, 'desc'], [1, 'asc']],
filterDropDown: filterDropDown:
{ {

View File

@@ -1,7 +1,6 @@
{% extends "allianceauth/base-bs5.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %} {% load static %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% load evelinks %} {% load evelinks %}
{% load navactive %} {% load navactive %}
@@ -87,12 +86,9 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %} {% include 'bundles/datatables-js-bs5.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
$('#tab_group_members').DataTable({ $('#tab_group_members').DataTable({
language: {url: '{{ DT_LANG_PATH }}'},
order: [[0, "asc"]], order: [[0, "asc"]],
columnDefs: [ columnDefs: [
{ {

View File

@@ -1,7 +1,6 @@
{% extends "allianceauth/base-bs5.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %} {% load static %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% block page_title %} {% block page_title %}
@@ -57,7 +56,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if g.group.authgroup.group_leader_groups.all.count %} {% if g.group.authgroup.group_leaders.all.count %}
{% for group in g.group.authgroup.group_leader_groups.all %} {% for group in g.group.authgroup.group_leader_groups.all %}
<span class="my-1 me-1 badge bg-secondary">{{group.name}}</span> <span class="my-1 me-1 badge bg-secondary">{{group.name}}</span>
{% endfor %} {% endfor %}
@@ -104,13 +103,9 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %} {% include 'bundles/datatables-js-bs5.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
$('#groupsTable').DataTable({ $('#groupsTable').DataTable();
language: {url: '{{ DT_LANG_PATH }}'},
});
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -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") logger.warning(f"User {request.user} attempted to view the membership of group {group_id} but permission was denied")
raise PermissionDenied raise PermissionDenied
except ObjectDoesNotExist as e: except ObjectDoesNotExist:
raise Http404("Group does not exist") from e raise Http404("Group does not exist")
render_items = {'group': group} render_items = {'group': group}
entries = RequestLog.objects.filter(group=group).order_by('-date') entries = RequestLog.objects.filter(group=group).order_by('-date')
render_items['entries'] = entries render_items['entries'] = entries
@@ -117,8 +117,8 @@ def group_membership_list(request, group_id):
) )
raise PermissionDenied raise PermissionDenied
except ObjectDoesNotExist as e: except ObjectDoesNotExist:
raise Http404("Group does not exist") from e raise Http404("Group does not exist")
group_leaders = group.authgroup.group_leaders.all() group_leaders = group.authgroup.group_leaders.all()
members = [] members = []

View File

@@ -1,3 +1,4 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models

View File

@@ -13,7 +13,7 @@ class ApplicationQuestion(models.Model):
help_text = models.CharField(max_length=254, blank=True) help_text = models.CharField(max_length=254, blank=True)
multi_select = models.BooleanField(default=False) multi_select = models.BooleanField(default=False)
def __str__(self) -> str: def __str__(self):
return "Question: " + self.title return "Question: " + self.title
@@ -21,7 +21,7 @@ class ApplicationChoice(models.Model):
question = models.ForeignKey(ApplicationQuestion,on_delete=models.CASCADE,related_name="choices") question = models.ForeignKey(ApplicationQuestion,on_delete=models.CASCADE,related_name="choices")
choice_text = models.CharField(max_length=200, verbose_name='Choice') choice_text = models.CharField(max_length=200, verbose_name='Choice')
def __str__(self) -> str: def __str__(self):
return self.choice_text return self.choice_text
@@ -29,7 +29,7 @@ class ApplicationForm(models.Model):
questions = SortedManyToManyField(ApplicationQuestion) questions = SortedManyToManyField(ApplicationQuestion)
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE) corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
def __str__(self) -> str: def __str__(self):
return str(self.corp) return str(self.corp)
@@ -50,7 +50,7 @@ class Application(models.Model):
('view_apis', 'Can view applicant APIs'),) ('view_apis', 'Can view applicant APIs'),)
unique_together = ('form', 'user') unique_together = ('form', 'user')
def __str__(self) -> str: def __str__(self):
return str(self.user) + " Application To " + str(self.form) return str(self.user) + " Application To " + str(self.form)
@property @property
@@ -77,7 +77,7 @@ class ApplicationResponse(models.Model):
answer = models.TextField() answer = models.TextField()
class Meta: class Meta:
unique_together = ('question', 'application') unique_together = ('question', 'application')
def __str__(self) -> str: def __str__(self):
return str(self.application) + " Answer To " + str(self.question) return str(self.application) + " Answer To " + str(self.question)
@@ -89,5 +89,5 @@ class ApplicationComment(models.Model):
text = models.TextField() text = models.TextField()
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str: def __str__(self):
return str(self.user) + " comment on " + str(self.application) return str(self.user) + " comment on " + str(self.application)

View File

@@ -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}") logger.debug(f"hr_application_view called by user {request.user} for app id {app_id}")
try: try:
app = Application.objects.prefetch_related('responses', 'comments', 'comments__user').get(pk=app_id) app = Application.objects.prefetch_related('responses', 'comments', 'comments__user').get(pk=app_id)
except Application.DoesNotExist as e: except Application.DoesNotExist:
raise Http404 from e raise Http404
if request.method == 'POST': if request.method == 'POST':
if request.user.has_perm('hrapplications.add_applicationcomment'): if request.user.has_perm('hrapplications.add_applicationcomment'):
form = HRApplicationCommentForm(request.POST) form = HRApplicationCommentForm(request.POST)

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ class MenuItem(models.Model):
) )
# app related properties # app related properties
hook_hash = models.CharField( # noqa: DJ001 hook_hash = models.CharField(
max_length=64, max_length=64,
default=None, default=None,
null=True, null=True,

View File

@@ -11,8 +11,8 @@
data-bs-target="#{{ item.html_id }}" data-bs-target="#{{ item.html_id }}"
aria-expanded="false" aria-expanded="false"
aria-controls="" aria-controls=""
{% endif %} {% endif %}>
></i> </i>
<a <a
class="nav-link flex-fill align-self-center me-auto {% if item.navactive %}{% navactive request item.navactive|join:' ' %}{% endif %}" class="nav-link flex-fill align-self-center me-auto {% if item.navactive %}{% navactive request item.navactive|join:' ' %}{% endif %}"
{% if item.is_folder %} {% if item.is_folder %}
@@ -36,7 +36,7 @@
{% if item.is_folder %} {% if item.is_folder %}
<span <span
class="pill m-2 align-self-center" class="pill m-2 align-self-center collapsed"
type="button" type="button"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target="#{{ item.html_id }}" data-bs-target="#{{ item.html_id }}"

View File

@@ -77,7 +77,7 @@ class RenderedMenuItem:
self.html_id = hook_obj.html_id self.html_id = hook_obj.html_id
def render_menu(request: HttpRequest) -> list[RenderedMenuItem]: # noqa: C901 def render_menu(request: HttpRequest) -> list[RenderedMenuItem]:
"""Return the rendered side menu for including in a template. """Return the rendered side menu for including in a template.
This function is creating BS5 style menus. This function is creating BS5 style menus.

View File

@@ -68,3 +68,35 @@ class TestDefaultDashboardWithSideMenu(TestCase):
self.assertEqual(links["/dashboard/"], "Dashboard") self.assertEqual(links["/dashboard/"], "Dashboard")
self.assertEqual(links["/groups/"], "Groups") self.assertEqual(links["/groups/"], "Groups")
self.assertNotIn("http://www.example.com/alpha", links) 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")

View File

@@ -1,21 +1 @@
"""
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 from .core import notify # noqa: F401

View File

@@ -15,7 +15,7 @@ class OpForm(forms.Form):
operation_name = forms.CharField(max_length=254, required=True, label=_("Operation Name")) operation_name = forms.CharField(max_length=254, required=True, label=_("Operation Name"))
type = forms.CharField(required=False, label=_("Operation Type")) type = forms.CharField(required=False, label=_("Operation Type"))
fc = forms.CharField(max_length=254, required=True, label=_("Fleet Commander")) fc = forms.CharField(max_length=254, required=True, label=_("Fleet Commander"))
duration = forms.CharField(max_length=25, required=True, label=_("Duration")) duration = forms.CharField(max_length=254, required=True, label=_("Duration"))
description = forms.CharField( description = forms.CharField(
widget=forms.Textarea(attrs={"rows": 10, "cols": 20, "input_type": "textarea"}), widget=forms.Textarea(attrs={"rows": 10, "cols": 20, "input_type": "textarea"}),
required=False, required=False,

View File

@@ -19,7 +19,7 @@ class OpTimerType(models.Model):
ordering = ['type'] ordering = ['type']
default_permissions = () default_permissions = ()
def __str__(self) -> str: def __str__(self):
return self.type return self.type
class OpTimer(models.Model): class OpTimer(models.Model):
@@ -39,5 +39,5 @@ class OpTimer(models.Model):
class Meta: class Meta:
ordering = ['start'] ordering = ['start']
default_permissions = () default_permissions = ()
def __str__(self) -> str: def __str__(self):
return self.operation_name return self.operation_name

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base-bs5.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% block page_title %} {% block page_title %}
@@ -58,14 +57,11 @@
{% include "bundles/datatables-js-bs5.html" %} {% include "bundles/datatables-js-bs5.html" %}
{% include "bundles/filterdropdown-js.html" %} {% include "bundles/filterdropdown-js.html" %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
const groupColumn = 0; const groupColumn = 0;
$('#tab_permissions_audit').DataTable({ $('#tab_permissions_audit').DataTable({
language: {url: '{{ DT_LANG_PATH }}'},
columnDefs: [{ columnDefs: [{
"visible": false, "visible": false,
"targets": groupColumn "targets": groupColumn

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base-bs5.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load aa_i18n %}
{% load i18n %} {% load i18n %}
{% block page_title %} {% block page_title %}
@@ -63,14 +62,11 @@
{% include "bundles/datatables-js-bs5.html" %} {% include "bundles/datatables-js-bs5.html" %}
{% include "bundles/filterdropdown-js.html" %} {% include "bundles/filterdropdown-js.html" %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script> <script>
$(document).ready(() => { $(document).ready(() => {
const groupColumn = 0; const groupColumn = 0;
$('#tab_permissions_overview').DataTable({ $('#tab_permissions_overview').DataTable({
language: {url: '{{ DT_LANG_PATH }}'},
columnDefs: [{ columnDefs: [{
"visible": false, "visible": false,
"targets": groupColumn "targets": groupColumn

View File

@@ -48,8 +48,8 @@ def permissions_audit(request, app_label, model, codename):
.prefetch_related('group_set', 'user_set', 'state_set', .prefetch_related('group_set', 'user_set', 'state_set',
'state_set__userprofile_set', 'group_set__user_set', 'state_set__userprofile_set__user')\ '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) .get(content_type__app_label=app_label, content_type__model=model, codename=codename)
except Permission.DoesNotExist as e: except Permission.DoesNotExist:
raise Http404 from e raise Http404
context = {'permission': { context = {'permission': {
'permission': perm, 'permission': perm,

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
if __name__ == "__main__":
def main() -> None: os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name}}.settings.local")
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as err:
raise ImportError( # The above import may fail for some other reason. Ensure that the
"Couldn't import Django. Are you sure it's installed and " # issue is really that Django is missing to avoid masking other
"available on your PYTHONPATH environment variable? Did you " # exceptions on Python 2.
"forget to activate a virtual environment?" try:
) from exc import django # noqa: F401
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from err
raise
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -42,34 +42,29 @@ INSTALLED_APPS = [
'allianceauth.theme.flatly', 'allianceauth.theme.flatly',
'allianceauth.theme.materia', 'allianceauth.theme.materia',
"allianceauth.custom_css", "allianceauth.custom_css",
'allianceauth.crontab',
'sri',
] ]
SRI_ALGORITHM = "sha512"
SECRET_KEY = "wow I'm a really bad default secret key" SECRET_KEY = "wow I'm a really bad default secret key"
# Celery configuration # Celery configuration
BROKER_URL = 'redis://localhost:6379/0' BROKER_URL = 'redis://localhost:6379/0'
CELERYBEAT_SCHEDULER = "allianceauth.crontab.schedulers.OffsetDatabaseScheduler" CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
CELERYBEAT_SCHEDULE = { CELERYBEAT_SCHEDULE = {
'esi_cleanup_callbackredirect': { 'esi_cleanup_callbackredirect': {
'task': 'esi.tasks.cleanup_callbackredirect', 'task': 'esi.tasks.cleanup_callbackredirect',
'schedule': crontab(minute='0', hour='*/4'), 'schedule': crontab(minute='0', hour='*/4'),
}, },
'esi_cleanup_token_subset': { # 1/48th * 1hr = 48Hr/2Day Refresh Cycles. 'esi_cleanup_token': {
'task': 'esi.tasks.cleanup_token_subset', 'task': 'esi.tasks.cleanup_token',
'schedule': crontab(minute="0", hour="*/1"), 'schedule': crontab(minute='0', hour='0'),
}, },
'run_model_update': { 'run_model_update': {
'task': 'allianceauth.eveonline.tasks.run_model_update', 'task': 'allianceauth.eveonline.tasks.run_model_update',
'schedule': crontab(minute='0', hour="*/6"), 'schedule': crontab(minute='0', hour="*/6"),
'apply_offset': True
}, },
'check_all_character_ownership': { 'check_all_character_ownership': {
'task': 'allianceauth.authentication.tasks.check_all_character_ownership', 'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
'schedule': crontab(minute='0', hour='*/4'), 'schedule': crontab(minute='0', hour='*/4'),
'apply_offset': True
}, },
'analytics_daily_stats': { 'analytics_daily_stats': {
'task': 'allianceauth.analytics.tasks.analytics_daily_stats', 'task': 'allianceauth.analytics.tasks.analytics_daily_stats',
@@ -77,7 +72,6 @@ CELERYBEAT_SCHEDULE = {
} }
} }
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR) BASE_DIR = os.path.dirname(PROJECT_DIR)
@@ -116,46 +110,6 @@ LANGUAGES = ( # Sorted by Language Code alphabetical order + English at top
("zh-hans", _("Simplified Chinese")), ("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 = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',

View File

@@ -33,6 +33,7 @@ class AbstractServiceModel(models.Model):
related_name='%(app_label)s' related_name='%(app_label)s'
) )
class Meta: class Meta:
abstract = True abstract = True
@@ -84,8 +85,8 @@ class ServicesCRUDMixin(SingleObjectMixin):
try: try:
return queryset.get(user__pk=self.request.user.pk) return queryset.get(user__pk=self.request.user.pk)
except ObjectDoesNotExist as e: except ObjectDoesNotExist:
raise Http404 from e raise Http404
class BaseDeactivateServiceAccountView(ServicesCRUDMixin, BaseServiceView, DeleteView): class BaseDeactivateServiceAccountView(ServicesCRUDMixin, BaseServiceView, DeleteView):

View File

@@ -119,7 +119,7 @@ class ServicesHook:
""" """
return '' return ''
def __str__(self) -> str: def __str__(self):
return self.name or 'Unknown Service Module' return self.name or 'Unknown Service Module'
class Urls: class Urls:

View File

@@ -26,7 +26,7 @@ class NameFormatConfig(models.Model):
"formatter for each state for each service." "formatter for each state for each service."
) )
def __str__(self) -> str: def __str__(self):
return '{}: {}'.format( return '{}: {}'.format(
self.service_name, ', '.join([str(x) for x in self.states.all()]) self.service_name, ', '.join([str(x) for x in self.states.all()])
) )

View File

@@ -2,7 +2,7 @@
of the current Alliance Auth instance. of the current Alliance Auth instance.
Example Example
------- =======
Here is an example for using the api to fetch the current roles from the configured Discord server. Here is an example for using the api to fetch the current roles from the configured Discord server.

View File

@@ -44,24 +44,26 @@ MAX_JITTER_PER_RUN_SECS = 1.0
def worker(num: int): def worker(num: int):
"""worker function""" """worker function"""
worker_info = f'worker {num}' worker_info = 'worker %d' % num
logger.info(f'{worker_info}: started') logger.info('%s: started', worker_info)
client = DiscordClient(DISCORD_BOT_TOKEN) client = DiscordClient(DISCORD_BOT_TOKEN)
try: try:
runs = 0 runs = 0
while runs < NUMBER_OF_RUNS: while runs < NUMBER_OF_RUNS:
run_info = f'{worker_info}: run {runs + 1}' run_info = '%s: run %d' % (worker_info, runs + 1)
my_jitter_secs = random() * MAX_JITTER_PER_RUN_SECS my_jitter_secs = random() * MAX_JITTER_PER_RUN_SECS
logger.info(f'{run_info} - waiting {my_jitter_secs:.3f} secs') logger.info('%s - waiting %s secs', run_info, f'{my_jitter_secs:.3f}')
sleep(my_jitter_secs) sleep(my_jitter_secs)
logger.info(f'{run_info} - started') logger.info('%s - started', run_info)
try: try:
client.modify_guild_member( client.modify_guild_member(
DISCORD_GUILD_ID, DISCORD_USER_ID, nick=NICK DISCORD_GUILD_ID, DISCORD_USER_ID, nick=NICK
) )
runs += 1 runs += 1
except DiscordApiBackoff as bo: except DiscordApiBackoff as bo:
message = f'{run_info} - waiting out API backoff for {bo.retry_after} ms' message = '%s - waiting out API backoff for %d ms' % (
run_info, bo.retry_after
)
logger.info(message) logger.info(message)
print() print()
print(message) print(message)

View File

@@ -89,12 +89,12 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
bo, bo,
bo.retry_after_seconds bo.retry_after_seconds
) )
raise self.retry(countdown=bo.retry_after_seconds) from bo raise self.retry(countdown=bo.retry_after_seconds)
except AttributeError as e: except AttributeError:
raise ValueError(f'{method} not a valid method for DiscordUser') from e raise ValueError(f'{method} not a valid method for DiscordUser')
except (HTTPError, ConnectionError) as e: except (HTTPError, ConnectionError):
logger.warning( logger.warning(
'%s failed for user %s, retrying in %d secs', '%s failed for user %s, retrying in %d secs',
method, method,
@@ -103,7 +103,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
exc_info=True exc_info=True
) )
if self.request.retries < DISCORD_TASKS_MAX_RETRIES: if self.request.retries < DISCORD_TASKS_MAX_RETRIES:
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE) from e raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE)
else: else:
logger.error( logger.error(
'%s failed for user %s after max retries', '%s failed for user %s after max retries',
@@ -192,8 +192,8 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any:
try: try:
result = getattr(DiscordUser.objects, method)(**kwargs) result = getattr(DiscordUser.objects, method)(**kwargs)
except AttributeError as e: except AttributeError:
raise ValueError(f'{method} not a valid method for DiscordUser.objects') from e raise ValueError(f'{method} not a valid method for DiscordUser.objects')
except DiscordApiBackoff as bo: except DiscordApiBackoff as bo:
logger.info( logger.info(
@@ -202,9 +202,9 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any:
bo, bo,
bo.retry_after_seconds bo.retry_after_seconds
) )
raise self.retry(countdown=bo.retry_after_seconds) from bo raise self.retry(countdown=bo.retry_after_seconds)
except (HTTPError, ConnectionError) as e: except (HTTPError, ConnectionError):
logger.warning( logger.warning(
'%s failed, retrying in %d secs', '%s failed, retrying in %d secs',
method, method,
@@ -212,7 +212,7 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any:
exc_info=True exc_info=True
) )
if self.request.retries < DISCORD_TASKS_MAX_RETRIES: if self.request.retries < DISCORD_TASKS_MAX_RETRIES:
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE) from e raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE)
else: else:
logger.error('%s failed after max retries', method, exc_info=True) logger.error('%s failed after max retries', method, exc_info=True)

View File

@@ -108,8 +108,9 @@ class DiscourseManager:
providers.discourse.client.activate(u_id) providers.discourse.client.activate(u_id)
@staticmethod @staticmethod
def __update_user(username, **kwargs) -> None: def __update_user(username, **kwargs):
providers.discourse.client.update_user(username, **kwargs) u_id = DiscourseManager.__user_name_to_id(username)
providers.discourse.client.update_user(endpoint, u_id, **kwargs)
@staticmethod @staticmethod
def __create_user(username, email, password): def __create_user(username, email, password):

View File

@@ -16,5 +16,5 @@ class DiscourseUser(models.Model):
("access_discourse", "Can access the Discourse service"), ("access_discourse", "Can access the Discourse service"),
) )
def __str__(self) -> str: def __str__(self):
return self.user.username return self.user.username

View File

@@ -52,7 +52,7 @@ class DiscourseTasks:
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
logger.warning(f"Discourse group sync failed for {user}, retrying in 10 mins") logger.warning(f"Discourse group sync failed for {user}, retrying in 10 mins")
raise self.retry(exc=e, countdown=60 * 10) from e raise self.retry(countdown=60 * 10)
logger.debug(f"Updated user {user} discourse groups.") logger.debug(f"Updated user {user} discourse groups.")
@staticmethod @staticmethod

View File

@@ -1,37 +0,0 @@
# 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",
),
),
]

View File

@@ -89,7 +89,7 @@ class MumbleUser(AbstractServiceModel):
max_length=254, max_length=254,
blank=True, blank=True,
editable=False, editable=False,
help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else." help_text="The Mumble Release the user last authenticated with"
) )
version = models.IntegerField( version = models.IntegerField(
verbose_name="Mumble Version", verbose_name="Mumble Version",
@@ -157,5 +157,4 @@ class MumbleUser(AbstractServiceModel):
class Meta: class Meta:
permissions = ( permissions = (
("access_mumble", "Can access the Mumble service"), ("access_mumble", "Can access the Mumble service"),
("view_connection_history", "Can access the connection history of the Mumble service"),
) )

View File

@@ -41,9 +41,9 @@ class MumbleTasks:
return True return True
except MumbleUser.DoesNotExist: except MumbleUser.DoesNotExist:
logger.info(f"Mumble group sync failed for {user}, user does not have a mumble account") logger.info(f"Mumble group sync failed for {user}, user does not have a mumble account")
except Exception as e: except Exception:
logger.exception(f"Mumble group sync failed for {user}, retrying in 10 mins") logger.exception(f"Mumble group sync failed for {user}, retrying in 10 mins")
raise self.retry(exc=e, countdown=60 * 10) from e raise self.retry(countdown=60 * 10)
else: else:
logger.debug(f"User {user} does not have a mumble account, skipping") logger.debug(f"User {user} does not have a mumble account, skipping")
return False return False
@@ -61,9 +61,9 @@ class MumbleTasks:
return True return True
except MumbleUser.DoesNotExist: except MumbleUser.DoesNotExist:
logger.info(f"Mumble display name sync failed for {user}, user does not have a mumble account") logger.info(f"Mumble display name sync failed for {user}, user does not have a mumble account")
except Exception as e: except Exception:
logger.exception(f"Mumble display name sync failed for {user}, retrying in 10 mins") logger.exception(f"Mumble display name sync failed for {user}, retrying in 10 mins")
raise self.retry(exc=e, countdown=60 * 10) from e raise self.retry(countdown=60 * 10)
else: else:
logger.debug(f"User {user} does not have a mumble account, skipping") logger.debug(f"User {user} does not have a mumble account, skipping")
return False return False

View File

@@ -1,232 +0,0 @@
{% 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 %}

Some files were not shown because too many files have changed in this diff Show More