mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 20:40:17 +02:00
Compare commits
270 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
34b94ae685 | ||
|
50fd900bdc | ||
|
1bf8ec5bc6 | ||
|
f849b75029 | ||
|
25c27793fe | ||
|
6e413772ad | ||
|
137a202e1b | ||
|
aaf718fe4d | ||
|
a193d9959b | ||
|
12250ef0c2 | ||
|
bde9802583 | ||
|
1b30b86d2b | ||
|
0707b9b98c | ||
|
b22a379db2 | ||
|
bb2e0aabbc | ||
|
449991d846 | ||
|
dd42c2b074 | ||
|
abff1b0add | ||
|
fc51f6bea2 | ||
|
6477c22308 | ||
|
329b3fecfb | ||
|
677505f22a | ||
|
f518166bd0 | ||
|
1f4c49f823 | ||
|
abcc4d47b5 | ||
|
3d4737df72 | ||
|
8f94885d8e | ||
|
993455d664 | ||
|
3cb0addee7 | ||
|
5530b76294 | ||
|
9fb51165ab | ||
|
a650f0730e | ||
|
63eb9edc9c | ||
|
d6e1eb9792 | ||
|
b459f96e6b | ||
|
bf32f2c1ef | ||
|
7ca67ebaae | ||
|
fa32f87a35 | ||
|
a630015451 | ||
|
bf43f59232 | ||
|
54910746e3 | ||
|
07ae68333d | ||
|
69e70a4c9b | ||
|
b55f11ee74 | ||
|
94ee3c0203 | ||
|
25cf329a50 | ||
|
b02827cb3f | ||
|
2bcc0570ad | ||
|
a3ea0c65a1 | ||
|
5e526da11c | ||
|
5c79265f90 | ||
|
eb0134e716 | ||
|
afde1f4729 | ||
|
5c6dda0eac | ||
|
af453bc772 | ||
|
e13674e886 | ||
|
e3e856b826 | ||
|
9d1cd23a8f | ||
|
148f7c116f | ||
|
33e7134d6f | ||
|
fb799551aa | ||
|
7b95051fe1 | ||
|
efb6a6db4f | ||
|
478aa1aa12 | ||
|
751e55ed6c | ||
|
9dad53f763 | ||
|
2d57064a7a | ||
|
833d12cf66 | ||
|
7b56caa4cb | ||
|
5752644122 | ||
|
cadc0cb534 | ||
|
dcdab5ae1f | ||
|
d64e896288 | ||
|
500d8ede32 | ||
|
f4c5c7f6db | ||
|
43e1be4032 | ||
|
702def2a4d | ||
|
a34baf4154 | ||
|
4de0774f15 | ||
|
83d2dfc7d9 | ||
|
c93afd2d68 | ||
|
b7bacd11af | ||
|
f26835fae0 | ||
|
4edb7cb678 | ||
|
2ce9ba997f | ||
|
055077fa77 | ||
|
f342ccbc6a | ||
|
37ffd0a1ac | ||
|
a1f705381e | ||
|
c0970ad4fa | ||
|
3818d0c6d1 | ||
|
95411c79cb | ||
|
eeccbbacfc | ||
|
f6c4180502 | ||
|
f4d3d6c0b1 | ||
|
e9d2d11297 | ||
|
96204b29e8 | ||
|
47842c1243 | ||
|
9b494106bc | ||
|
d51e730a7f | ||
|
363909c0c4 | ||
|
82273f68fe | ||
|
12fa38b446 | ||
|
c26af593ff | ||
|
8e9a53c494 | ||
|
5559ce5fbb | ||
|
faa529a55b | ||
|
4ccfe20c14 | ||
|
960c9625fe | ||
|
7b92d103d6 | ||
|
c1e2449084 | ||
|
3acb651650 | ||
|
2de57b334b | ||
|
0498f5bb1b | ||
|
929485a8f9 | ||
|
28cb62f373 | ||
|
02214b74d0 | ||
|
f497c18e5b | ||
|
cb57d922e6 | ||
|
805d138b09 | ||
|
09a583fb1d | ||
|
146c4c8d94 | ||
|
c2ae680f72 | ||
|
b5ad1c8a1a | ||
|
8be2760fc4 | ||
|
f047943eb7 | ||
|
43906f41b3 | ||
|
a18ec98877 | ||
|
14163d2c0c | ||
|
81d9c41cf6 | ||
|
58f5a5b41d | ||
|
6363bb706a | ||
|
baf3be4cb2 | ||
|
e69444fe79 | ||
|
7483fcb876 | ||
|
a57d55504d | ||
|
affb30e9f4 | ||
|
588cb1b7ca | ||
|
a609faa91b | ||
|
856e939c21 | ||
|
1b6cf98885 | ||
|
92c2af9975 | ||
|
5ef70bb031 | ||
|
60998bffc2 | ||
|
a5971314f5 | ||
|
a03c766840 | ||
|
ad47ff2c54 | ||
|
3efdb8f12b | ||
|
823fc82d19 | ||
|
a93e510895 | ||
|
d99f5858d8 | ||
|
4578ecf21d | ||
|
b737504d52 | ||
|
c6b6443901 | ||
|
f51523dc07 | ||
|
bd4dd60c98 | ||
|
a4ea48e14e | ||
|
646d3f5408 | ||
|
0f057ffa84 | ||
|
7033406ba6 | ||
|
6b395ca1d4 | ||
|
795a7e006f | ||
|
2a894cd62c | ||
|
9ada26e849 | ||
|
7120b3956c | ||
|
4da67cfaf6 | ||
|
0a940810bd | ||
|
a868438492 | ||
|
dc1ed8c570 | ||
|
8489f204dd | ||
|
1478588016 | ||
|
a16eb4b7f7 | ||
|
292fb7b29d | ||
|
c6890dd2c6 | ||
|
702564d15e | ||
|
cef2e86ea1 | ||
|
50681b023b | ||
|
2822775fb8 | ||
|
ef7c8be7b5 | ||
|
d639617eba | ||
|
2125192f72 | ||
|
8d63801b00 | ||
|
e053fb7d96 | ||
|
ae7ed5c297 | ||
|
d624ba4427 | ||
|
164cd4fbb2 | ||
|
94b52c850e | ||
|
4d19ceb388 | ||
|
9a64728311 | ||
|
dcc0c78daf | ||
|
5507c30af4 | ||
|
66b97835d4 | ||
|
fd66a7cb20 | ||
|
c0f0f8db73 | ||
|
4210b2eabc | ||
|
225e68647e | ||
|
7e2f864ebf | ||
|
f2384ba45b | ||
|
c5918b9b3c | ||
|
ffedc4103d | ||
|
0467b23a1a | ||
|
cda5ce739f | ||
|
e5c8426ea3 | ||
|
b2bd489ddc | ||
|
6397cf358a | ||
|
90fcc4a811 | ||
|
ec7472fe22 | ||
|
5148b1914d | ||
|
2e0716f5ae | ||
|
1fb091acb2 | ||
|
07c62ed32a | ||
|
660fe79d08 | ||
|
42ee06470c | ||
|
69aaa9652f | ||
|
1ccfff50e5 | ||
|
57a39557fd | ||
|
c36dea08e3 | ||
|
d3acd821b7 | ||
|
0a17427169 | ||
|
ce8935e621 | ||
|
efff946a56 | ||
|
1dea92ed76 | ||
|
3f54d49d8b | ||
|
77da6928b2 | ||
|
bd17b95cac | ||
|
4ed1c5b7c4 | ||
|
271fd8e8c4 | ||
|
9b4321281a | ||
|
052c35c8e5 | ||
|
0fcb517b0b | ||
|
dcfddf0add | ||
|
4a4258d0e6 | ||
|
dd15a221aa | ||
|
737e02293a | ||
|
c34efebacf | ||
|
4602097399 | ||
|
7051e06564 | ||
|
9767ce79d8 | ||
|
0c090f1486 | ||
|
618ee81f9b | ||
|
98efb9f887 | ||
|
cbe6c821cc | ||
|
de9d2b39a6 | ||
|
0d5f22288b | ||
|
e0d76dc268 | ||
|
ecc9e68330 | ||
|
710149ec21 | ||
|
3c2c137dad | ||
|
a8271c4189 | ||
|
3315ae7778 | ||
|
d2f048f8fe | ||
|
0fe2855faa | ||
|
79a1fa3d7c | ||
|
96fe88d5c7 | ||
|
59391ad3c5 | ||
|
94e9c08422 | ||
|
acff3695bc | ||
|
43ec8514aa | ||
|
4c629b193f | ||
|
c651da4011 | ||
|
da382cffd1 | ||
|
4ecfc3afd8 | ||
|
4eb7dbbe62 | ||
|
c96ba65296 | ||
|
ff2f60f7f3 | ||
|
3000545c98 | ||
|
f3ad092ef2 | ||
|
a012e7df2f | ||
|
1fa77412c0 | ||
|
e56caeb22b |
@ -3,22 +3,41 @@
|
|||||||
# 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/
|
||||||
|
)
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
# Code Upgrades
|
# Code Upgrades
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.15.2
|
rev: v3.20.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py38-plus]
|
args: [--py38-plus]
|
||||||
- repo: https://github.com/adamchainz/django-upgrade
|
- repo: https://github.com/adamchainz/django-upgrade
|
||||||
rev: 1.17.0
|
rev: 1.25.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: django-upgrade
|
- id: django-upgrade
|
||||||
args: [--target-version=4.2]
|
args: [--target-version=4.2]
|
||||||
|
|
||||||
# Formatting
|
# Formatting
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
# Identify invalid files
|
# Identify invalid files
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
@ -33,9 +52,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
|
||||||
@ -44,45 +63,22 @@ 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: 2.7.3
|
rev: 3.2.1
|
||||||
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.41.0
|
rev: v0.45.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: 2.1.3
|
rev: v2.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
name: pyproject.toml formatter
|
name: pyproject.toml formatter
|
||||||
@ -90,9 +86,9 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- --indent=4
|
- --indent=4
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- tox==4.15.0 # https://github.com/tox-dev/tox/releases/latest
|
- tox==4.24.1 # https://github.com/tox-dev/tox/releases/latest
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
rev: v0.18
|
rev: v0.24.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate-pyproject
|
- id: validate-pyproject
|
||||||
name: Validate pyproject.toml
|
name: Validate pyproject.toml
|
||||||
|
38
README.md
38
README.md
@ -1,20 +1,20 @@
|
|||||||
# Alliance Auth
|
# Alliance Auth
|
||||||
|
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||||
[](http://allianceauth.readthedocs.io/?badge=latest)
|
[](https://allianceauth.readthedocs.io/?badge=latest)
|
||||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||||
[](https://discord.gg/fjnHAmk)
|
[](https://discord.gg/fjnHAmk)
|
||||||
|
|
||||||
An auth system for EVE Online to help in-game organizations manage online service access.
|
A flexible authentication platform for EVE Online to help in-game organizations manage access to applications and services. AA provides both, a stable core, and a robust framework for community development and custom applications.
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Documentation](http://allianceauth.rtfd.io)
|
- [Documentation](https://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)
|
||||||
@ -22,29 +22,35 @@ An auth system for EVE Online to help in-game organizations manage online servic
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Alliance Auth (AA) is a web site that helps Eve Online organizations efficiently manage access to applications and services.
|
Alliance Auth (AA) is a platform that helps Eve Online organizations efficiently manage access to applications and services.
|
||||||
|
|
||||||
Main features:
|
Main features:
|
||||||
|
|
||||||
- Automatically grants or revokes user access to external services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
|
- Automatically grants or revokes user access to external services (e.g.: Discord, Mumble) based on the user's current membership to [a variety of EVE Online affiliation](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
|
||||||
|
|
||||||
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
|
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
|
||||||
|
|
||||||
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
|
- Includes a set of connectors (called ["Services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
|
||||||
|
|
||||||
- Includes a set of web [apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
|
- Includes a set of web [Apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
|
||||||
|
|
||||||
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
||||||
|
|
||||||
- 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](http://allianceauth.rtfd.io).
|
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](https://allianceauth.rtfd.io).
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
Here is an example of the Alliance Auth web site with some plug-ins apps and services enabled:
|
Here is an example of the Alliance Auth web site with a mixture of Services, Apps and Community Creations enabled:
|
||||||
|
|
||||||

|
### Flatly Theme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Darkly Theme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
@ -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__ = '4.2.1'
|
__version__ = '4.8.0'
|
||||||
__title__ = 'Alliance Auth'
|
__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__}'
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import AnalyticsIdentifier, AnalyticsTokens
|
from .models import AnalyticsIdentifier, AnalyticsTokens
|
||||||
|
from solo.admin import SingletonModelAdmin
|
||||||
|
|
||||||
|
|
||||||
@admin.register(AnalyticsIdentifier)
|
@admin.register(AnalyticsIdentifier)
|
||||||
class AnalyticsIdentifierAdmin(admin.ModelAdmin):
|
class AnalyticsIdentifierAdmin(SingletonModelAdmin):
|
||||||
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', ]
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsConfig(AppConfig):
|
class AnalyticsConfig(AppConfig):
|
||||||
name = 'allianceauth.analytics'
|
name = 'allianceauth.analytics'
|
||||||
label = 'analytics'
|
label = 'analytics'
|
||||||
|
verbose_name = _('Analytics')
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-12-11 02:17
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0009_remove_analyticstokens_ignore_paths_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='analyticsidentifier',
|
||||||
|
options={'verbose_name': 'Analytics Identifier'},
|
||||||
|
),
|
||||||
|
]
|
@ -1,23 +1,19 @@
|
|||||||
|
from typing import Literal
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from solo.models import SingletonModel
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsIdentifier(models.Model):
|
class AnalyticsIdentifier(SingletonModel):
|
||||||
|
|
||||||
identifier = models.UUIDField(
|
identifier = models.UUIDField(default=uuid4, editable=False)
|
||||||
default=uuid4,
|
|
||||||
editable=False)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def __str__(self) -> Literal['Analytics Identifier']:
|
||||||
if not self.pk and AnalyticsIdentifier.objects.exists():
|
return "Analytics Identifier"
|
||||||
# Force a single object
|
|
||||||
raise ValidationError('There is can be only one \
|
class Meta:
|
||||||
AnalyticsIdentifier instance')
|
verbose_name = "Analytics Identifier"
|
||||||
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
|
|
||||||
return super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsTokens(models.Model):
|
class AnalyticsTokens(models.Model):
|
||||||
|
@ -5,6 +5,7 @@ from django.apps import apps
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from .models import AnalyticsTokens, AnalyticsIdentifier
|
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
existence_baremetal_or_docker,
|
||||||
install_stat_addons,
|
install_stat_addons,
|
||||||
install_stat_tokens,
|
install_stat_tokens,
|
||||||
install_stat_users)
|
install_stat_users)
|
||||||
@ -67,8 +68,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():
|
def analytics_daily_stats() -> None:
|
||||||
"""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,6 +78,7 @@ def analytics_daily_stats():
|
|||||||
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',
|
||||||
@ -84,6 +86,11 @@ def analytics_daily_stats():
|
|||||||
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',
|
||||||
@ -99,7 +106,6 @@ def analytics_daily_stats():
|
|||||||
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",
|
||||||
@ -135,7 +141,7 @@ def analytics_daily_stats():
|
|||||||
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,
|
||||||
@ -165,7 +171,7 @@ def send_ga_tracking_celery_event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex,
|
'client_id': AnalyticsIdentifier.get_solo().identifier.hex,
|
||||||
"user_properties": {
|
"user_properties": {
|
||||||
"allianceauth_version": {
|
"allianceauth_version": {
|
||||||
"value": __version__
|
"value": __version__
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from allianceauth.analytics.models import AnalyticsIdentifier
|
from allianceauth.analytics.models import AnalyticsIdentifier
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
from django.test.testcases import TestCase
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
from uuid import UUID, uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
# Identifiers
|
# Identifiers
|
||||||
@ -14,14 +13,4 @@ uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
|
|||||||
class TestAnalyticsIdentifier(TestCase):
|
class TestAnalyticsIdentifier(TestCase):
|
||||||
|
|
||||||
def test_identifier_random(self):
|
def test_identifier_random(self):
|
||||||
self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4)
|
self.assertNotEqual(AnalyticsIdentifier.get_solo(), 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))
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from allianceauth.authentication.models import User
|
from allianceauth.authentication.models import User
|
||||||
from esi.models import Token
|
from esi.models import Token
|
||||||
@ -34,3 +35,16 @@ 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"
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.checks import Warning, Error, register
|
|
||||||
|
|
||||||
|
|
||||||
class AllianceAuthConfig(AppConfig):
|
class AllianceAuthConfig(AppConfig):
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.checks import register, Tags
|
from django.core.checks import register, Tags
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationConfig(AppConfig):
|
class AuthenticationConfig(AppConfig):
|
||||||
name = "allianceauth.authentication"
|
name = "allianceauth.authentication"
|
||||||
label = "authentication"
|
label = "authentication"
|
||||||
|
verbose_name = _("Authentication")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from allianceauth.authentication import checks, signals # noqa: F401
|
from allianceauth.authentication import checks, signals # noqa: F401
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2 on 2024-09-13 09:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0023_alter_userprofile_language'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(blank=True, choices=[('en', 'English'), ('cs-cz', 'Czech'), ('de', 'German'), ('es', 'Spanish'), ('it-it', 'Italian'), ('ja', 'Japanese'), ('ko-kr', 'Korean'), ('fr-fr', 'French'), ('ru', 'Russian'), ('nl-nl', 'Dutch'), ('pl-pl', 'Polish'), ('uk', 'Ukrainian'), ('zh-hans', 'Simplified Chinese')], default='', max_length=10, verbose_name='Language'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
@ -27,7 +28,7 @@ class State(models.Model):
|
|||||||
help_text="Factions to whose members this state is available.")
|
help_text="Factions to whose members this state is available.")
|
||||||
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
||||||
|
|
||||||
objects = StateManager()
|
objects: ClassVar[StateManager] = StateManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-priority']
|
ordering = ['-priority']
|
||||||
@ -67,18 +68,20 @@ class UserProfile(models.Model):
|
|||||||
"""
|
"""
|
||||||
Choices for UserProfile.language
|
Choices for UserProfile.language
|
||||||
"""
|
"""
|
||||||
|
# Sorted by Language Code alphabetical order + English at top
|
||||||
ENGLISH = 'en', _('English')
|
ENGLISH = 'en', _('English')
|
||||||
|
CZECH = 'cs-cz', _("Czech") # Not yet at 50% translated
|
||||||
GERMAN = 'de', _('German')
|
GERMAN = 'de', _('German')
|
||||||
SPANISH = 'es', _('Spanish')
|
SPANISH = 'es', _('Spanish')
|
||||||
CHINESE = 'zh-hans', _('Chinese Simplified')
|
ITALIAN = 'it-it', _('Italian')
|
||||||
RUSSIAN = 'ru', _('Russian')
|
|
||||||
KOREAN = 'ko', _('Korean')
|
|
||||||
FRENCH = 'fr', _('French')
|
|
||||||
JAPANESE = 'ja', _('Japanese')
|
JAPANESE = 'ja', _('Japanese')
|
||||||
ITALIAN = 'it', _('Italian')
|
KOREAN = 'ko-kr', _('Korean')
|
||||||
|
FRENCH = 'fr-fr', _('French')
|
||||||
|
RUSSIAN = 'ru', _('Russian')
|
||||||
|
DUTCH = 'nl-nl', _("Dutch")
|
||||||
|
POLISH = 'pl-pl', _("Polish")
|
||||||
UKRAINIAN = 'uk', _('Ukrainian')
|
UKRAINIAN = 'uk', _('Ukrainian')
|
||||||
POLISH = 'pl', _("Polish")
|
CHINESE = 'zh-hans', _('Simplified Chinese')
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
User,
|
User,
|
||||||
@ -135,8 +138,10 @@ class UserProfile(models.Model):
|
|||||||
sender=self.__class__, user=self.user, state=self.state
|
sender=self.__class__, user=self.user, state=self.state
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
|
|
||||||
class CharacterOwnership(models.Model):
|
class CharacterOwnership(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ('change', 'delete')
|
default_permissions = ('change', 'delete')
|
||||||
@ -146,7 +151,7 @@ class CharacterOwnership(models.Model):
|
|||||||
owner_hash = models.CharField(max_length=28, unique=True)
|
owner_hash = models.CharField(max_length=28, unique=True)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
|
||||||
|
|
||||||
objects = CharacterOwnershipManager()
|
objects: ClassVar[CharacterOwnershipManager] = CharacterOwnershipManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user}: {self.character}"
|
return f"{self.user}: {self.character}"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "allianceauth/base-bs5.html" %}
|
{% extends "allianceauth/base-bs5.html" %}
|
||||||
|
|
||||||
|
{% load aa_i18n %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
@ -13,7 +14,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://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
|
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://developers.eveonline.com/authorized-apps where possible."|urlize %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="table w-100" id="table_tokens">
|
<table class="table w-100" id="table_tokens">
|
||||||
@ -30,7 +31,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="white-space:initial;">
|
<td style="white-space:initial;">
|
||||||
{% for s in t.scopes.all %}
|
{% for s in t.scopes.all %}
|
||||||
<span class="badge bg-secondary">{{ s.name }}</span>
|
<span class="badge text-bg-secondary">{{ s.name }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -50,20 +51,23 @@
|
|||||||
{% 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;
|
||||||
|
|
||||||
const table = $('#table_tokens').DataTable({
|
$('#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) {
|
||||||
var api = this.api();
|
const api = this.api();
|
||||||
var rows = api.rows({page: 'current'}).nodes();
|
const rows = api.rows({page: 'current'}).nodes();
|
||||||
var last = null;
|
let last = null;
|
||||||
api.column(grp, {page: 'current'})
|
api.column(grp, {page: 'current'})
|
||||||
.data()
|
.data()
|
||||||
.each((group, i) => {
|
.each((group, i) => {
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
{% load theme_tags %}
|
{% load theme_tags %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" {% theme_html_tags %}>
|
||||||
<head>
|
<head>
|
||||||
|
<!-- Required meta tags -->
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="description" content="">
|
<!-- End Required meta tags -->
|
||||||
<meta name="author" content="">
|
|
||||||
<!-- TODO Bundle all the site specific stuff up into its own template for easy override -->
|
|
||||||
<meta property="og:title" content="{{ SITE_NAME }}">
|
|
||||||
<meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
|
||||||
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
|
|
||||||
|
|
||||||
|
<!-- Meta tags -->
|
||||||
|
{% include 'allianceauth/opengraph.html' %}
|
||||||
{% include 'allianceauth/icons.html' %}
|
{% include 'allianceauth/icons.html' %}
|
||||||
|
<!-- Meta tags -->
|
||||||
|
|
||||||
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
|
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
|
||||||
|
|
||||||
{% theme_css %}
|
{% theme_css %}
|
||||||
|
|
||||||
{% include 'bundles/fontawesome.html' %}
|
{% include 'bundles/fontawesome.html' %}
|
||||||
|
{% include 'bundles/auth-framework-css.html' %}
|
||||||
|
|
||||||
{% block extra_include %}
|
{% block extra_include %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div class="dropdown">
|
<form class="dropdown-item" action="{% url 'set_language' %}" method="post">
|
||||||
<form action="{% url 'set_language' %}" method="post">
|
|
||||||
{% 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_language_info_list for LANGUAGES as languages %}
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
|
||||||
{% for language in languages %}
|
{% for lang_code, lang_name in LANGUAGES %}
|
||||||
<option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
<option lang="{{ lang_code }}" value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
{{ language.name_local|capfirst }} ({{ language.code }})
|
{{ lang_code|language_name_local|capfirst }} ({{ lang_code }})
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<a class="text-reset" href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
|
<a class="text-reset" href="https://developers.eveonline.com/authorized-apps" target="_blank" rel="noopener noreferrer">
|
||||||
{% translate "Manage ESI Applications" %}
|
{% translate "Manage ESI Applications" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Django system checks for Alliance Auth
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from django import db
|
from django import db
|
||||||
from django.core.checks import CheckMessage, Error, register, Warning
|
from django.core.checks import CheckMessage, Error, register, Warning
|
||||||
@ -16,142 +20,475 @@ B = Configuration
|
|||||||
|
|
||||||
@register()
|
@register()
|
||||||
def django_settings(app_configs, **kwargs) -> List[CheckMessage]:
|
def django_settings(app_configs, **kwargs) -> List[CheckMessage]:
|
||||||
errors: List[CheckMessage] = []
|
"""
|
||||||
if hasattr(settings, "SITE_URL"):
|
Check that Django settings are correctly configured
|
||||||
if settings.SITE_URL[-1] == "/":
|
|
||||||
errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005"))
|
|
||||||
else:
|
|
||||||
errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006"))
|
|
||||||
|
|
||||||
if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"):
|
:param app_configs:
|
||||||
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
|
:type app_configs:
|
||||||
errors.append(Warning("'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", hint="", id="allianceauth.checks.B007"))
|
:param kwargs:
|
||||||
|
:type kwargs:
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
errors: List[CheckMessage] = []
|
||||||
|
|
||||||
|
# Check for SITE_URL
|
||||||
|
if hasattr(settings, "SITE_URL"):
|
||||||
|
# Check if SITE_URL is empty
|
||||||
|
if settings.SITE_URL == "":
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="'SITE_URL' is empty.",
|
||||||
|
hint="Make sure to set 'SITE_URL' to the URL of your Auth instance. (Without trailing slash)",
|
||||||
|
id="allianceauth.checks.B011",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check if SITE_URL has a trailing slash
|
||||||
|
elif settings.SITE_URL[-1] == "/":
|
||||||
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg="'SITE_URL' has a trailing slash. This may lead to incorrect links being generated by Auth.",
|
||||||
|
hint="",
|
||||||
|
id="allianceauth.checks.B005",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# SITE_URL not found
|
||||||
else:
|
else:
|
||||||
errors.append(Error("No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", hint="", id="allianceauth.checks.B008"))
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.",
|
||||||
|
hint="",
|
||||||
|
id="allianceauth.checks.B006",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for CSRF_TRUSTED_ORIGINS
|
||||||
|
if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"):
|
||||||
|
# Check if SITE_URL is not in CSRF_TRUSTED_ORIGINS
|
||||||
|
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
|
||||||
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg="'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.",
|
||||||
|
hint="",
|
||||||
|
id="allianceauth.checks.B007",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# CSRF_TRUSTED_ORIGINS not found
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified",
|
||||||
|
hint="",
|
||||||
|
id="allianceauth.checks.B008",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for ESI_USER_CONTACT_EMAIL
|
||||||
|
if hasattr(settings, "ESI_USER_CONTACT_EMAIL"):
|
||||||
|
# Check if ESI_USER_CONTACT_EMAIL is empty
|
||||||
|
if settings.ESI_USER_CONTACT_EMAIL == "":
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.",
|
||||||
|
hint="",
|
||||||
|
id="allianceauth.checks.B009",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# ESI_USER_CONTACT_EMAIL not found
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.",
|
||||||
|
hint="",
|
||||||
|
id="allianceauth.checks.B010",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
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 redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc):
|
if (
|
||||||
errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A001"))
|
redis_version.major == 7
|
||||||
|
and redis_version.minor == 2
|
||||||
|
and timezone.now()
|
||||||
|
> timezone.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(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A002"))
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
|
||||||
|
hint=allianceauth_redis_install_link,
|
||||||
|
id="allianceauth.checks.A002",
|
||||||
|
)
|
||||||
|
)
|
||||||
elif redis_version.major == 6 and redis_version.minor == 2:
|
elif redis_version.major == 6 and redis_version.minor == 2:
|
||||||
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A018"))
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
|
||||||
|
hint=allianceauth_redis_install_link,
|
||||||
|
id="allianceauth.checks.A018",
|
||||||
|
)
|
||||||
|
)
|
||||||
elif redis_version.major in [6, 5]:
|
elif redis_version.major in [6, 5]:
|
||||||
errors.append(Error(f"Redis {redis_version.public} EOL", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A003"))
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"Redis {redis_version.public} EOL",
|
||||||
|
hint=allianceauth_redis_install_link,
|
||||||
|
id="allianceauth.checks.A003",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
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(".".join(str(i) for i in connection.mysql_version))
|
mysql_version = Pep440Version(
|
||||||
|
".".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 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=timezone.utc):
|
if mysql_version.major == 8:
|
||||||
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"))
|
if mysql_version.minor == 4 and timezone.now() > timezone.datetime(
|
||||||
elif mysql_version.major == 8 and mysql_version.minor == 3:
|
year=2032, month=4, day=30, tzinfo=timezone.utc
|
||||||
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005"))
|
):
|
||||||
elif mysql_version.major == 8 and mysql_version.minor == 2:
|
errors.append(
|
||||||
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"))
|
Error(
|
||||||
elif mysql_version.major == 8 and mysql_version.minor == 1:
|
msg=f"MySQL {mysql_version.public} EOL",
|
||||||
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"))
|
hint=mysql_quick_guide_link,
|
||||||
elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=timezone.utc):
|
id="allianceauth.checks.A004",
|
||||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008"))
|
)
|
||||||
elif mysql_version.major < 8: # This will also catch Mariadb 5.x
|
)
|
||||||
errors.append(Error(f"MySQL or MariaDB {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A009"))
|
elif mysql_version.minor == 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 == 2:
|
||||||
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg=f"MySQL {mysql_version.public} Non LTS",
|
||||||
|
hint=mysql_quick_guide_link,
|
||||||
|
id="allianceauth.checks.A006",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif mysql_version.minor == 1:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"MySQL {mysql_version.public} EOL",
|
||||||
|
hint=mysql_quick_guide_link,
|
||||||
|
id="allianceauth.checks.A007",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif mysql_version.minor == 0 and timezone.now() > timezone.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]:
|
def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]:
|
||||||
|
"""
|
||||||
|
Check that MariaDB is a supported version
|
||||||
|
|
||||||
|
:param app_configs:
|
||||||
|
:type app_configs:
|
||||||
|
:param kwargs:
|
||||||
|
:type kwargs:
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
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():
|
||||||
if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB
|
# TODO: Find a way to determine MySQL vs. MariaDB
|
||||||
|
if connection.vendor == "mysql":
|
||||||
try:
|
try:
|
||||||
mariadb_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
|
mariadb_version = Pep440Version(
|
||||||
|
".".join(str(i) for i in connection.mysql_version)
|
||||||
|
)
|
||||||
except InvalidVersion:
|
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 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=timezone.utc):
|
if mariadb_version.major == 11:
|
||||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010"))
|
if mariadb_version.minor == 4 and timezone.now() > timezone.datetime(
|
||||||
elif mariadb_version.major == 11 and mariadb_version.minor == 2:
|
year=2029, month=5, day=19, tzinfo=timezone.utc
|
||||||
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018"))
|
):
|
||||||
if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=timezone.utc):
|
errors.append(
|
||||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011"))
|
Error(
|
||||||
elif mariadb_version.major == 11 and mariadb_version.minor == 1:
|
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||||
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019"))
|
hint=mariadb_download_link,
|
||||||
if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=timezone.utc):
|
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"))
|
elif mariadb_version.minor == 2:
|
||||||
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg=f"MariaDB {mariadb_version.public} Non LTS",
|
||||||
|
hint=mariadb_download_link,
|
||||||
|
id="allianceauth.checks.A018",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if timezone.now() > timezone.datetime(
|
||||||
|
year=2024, month=11, day=21, tzinfo=timezone.utc
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||||
|
hint=mariadb_download_link,
|
||||||
|
id="allianceauth.checks.A011",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif mariadb_version.minor == 1:
|
||||||
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
msg=f"MariaDB {mariadb_version.public} Non LTS",
|
||||||
|
hint=mariadb_download_link,
|
||||||
|
id="allianceauth.checks.A019",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if timezone.now() > timezone.datetime(
|
||||||
|
year=2024, month=8, day=21, tzinfo=timezone.utc
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||||
|
hint=mariadb_download_link,
|
||||||
|
id="allianceauth.checks.A012",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Demote versions down here once EOL
|
||||||
|
elif mariadb_version.minor in [0, 3]:
|
||||||
|
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 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=timezone.utc):
|
elif mariadb_version.major == 10:
|
||||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014"))
|
if mariadb_version.minor == 11 and timezone.now() > timezone.datetime(
|
||||||
elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=timezone.utc):
|
year=2028, month=2, day=10, tzinfo=timezone.utc
|
||||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015"))
|
):
|
||||||
elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=timezone.utc):
|
errors.append(
|
||||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016"))
|
Error(
|
||||||
elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL
|
msg=f"MariaDB {mariadb_version.public} EOL",
|
||||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017"))
|
hint=mariadb_download_link,
|
||||||
|
id="allianceauth.checks.A014",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif mariadb_version.minor == 6 and timezone.now() > timezone.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 timezone.now() > timezone.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(".".join(str(i) for i in sqlite_version_info))
|
sqlite_version = Pep440Version(
|
||||||
|
".".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(Error(f"SQLite {sqlite_version.public} Unsupported by Django", hint="https://pkgs.org/download/sqlite3", id="allianceauth.checks.A020"))
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"SQLite {sqlite_version.public} Unsupported by Django",
|
||||||
|
hint="https://pkgs.org/download/sqlite3",
|
||||||
|
id="allianceauth.checks.A020",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
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(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}",
|
||||||
|
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||||
|
id="allianceauth.checks.B001",
|
||||||
|
)
|
||||||
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}",
|
||||||
|
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||||
|
id="allianceauth.checks.B001",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# This hasn't actually been set on AA yet
|
# This hasn't actually been set on AA yet
|
||||||
# try:
|
# try:
|
||||||
# if connection.settings_dict["OPTIONS"]["collation"] != "utf8mb4_unicode_ci":
|
# if (
|
||||||
# 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"))
|
# connection.settings_dict["OPTIONS"]["collation"]
|
||||||
|
# != "utf8mb4_unicode_ci"
|
||||||
|
# ):
|
||||||
|
# errors.append(
|
||||||
|
# Error(
|
||||||
|
# msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}",
|
||||||
|
# hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||||
|
# id="allianceauth.checks.B001",
|
||||||
|
# )
|
||||||
|
# )
|
||||||
# except KeyError:
|
# except KeyError:
|
||||||
# errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
# errors.append(
|
||||||
|
# Error(
|
||||||
|
# msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}",
|
||||||
|
# hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
|
||||||
|
# id="allianceauth.checks.B001",
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
# if connection.vendor == "sqlite":
|
# if connection.vendor == "sqlite":
|
||||||
|
|
||||||
@ -160,12 +497,58 @@ 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]:
|
||||||
errors: List[CheckMessage] = []
|
"""
|
||||||
if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}:
|
Check that Celery settings are correctly configured
|
||||||
errors.append(Error("Celery Priorities are not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
|
|
||||||
|
:param app_configs:
|
||||||
|
:type app_configs:
|
||||||
|
:param kwargs:
|
||||||
|
:type kwargs:
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
errors: List[CheckMessage] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if current_app.conf.broker_transport_options != {
|
||||||
|
"priority_steps": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||||
|
"queue_order_strategy": "priority",
|
||||||
|
}:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="Celery Priorities are not set correctly",
|
||||||
|
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
|
||||||
|
id="allianceauth.checks.B003",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="Celery Priorities are not set",
|
||||||
|
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
|
||||||
|
id="allianceauth.checks.B003",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not current_app.conf.broker_connection_retry_on_startup:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="Celery broker_connection_retry_on_startup not set correctly",
|
||||||
|
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
|
||||||
|
id="allianceauth.checks.B004",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
msg="Celery broker_connection_retry_on_startup not set",
|
||||||
|
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
|
||||||
|
id="allianceauth.checks.B004",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if current_app.conf.broker_connection_retry_on_startup != True:
|
|
||||||
errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class CorpUtilsConfig(AppConfig):
|
class CorpUtilsConfig(AppConfig):
|
||||||
name = 'allianceauth.corputils'
|
name = 'allianceauth.corputils'
|
||||||
label = 'corputils'
|
label = 'corputils'
|
||||||
|
verbose_name = _('Corporation Stats')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
from allianceauth.authentication.models import CharacterOwnership, UserProfile
|
from allianceauth.authentication.models import CharacterOwnership, UserProfile
|
||||||
from bravado.exception import HTTPForbidden
|
from bravado.exception import HTTPForbidden
|
||||||
@ -40,9 +41,9 @@ class CorpStats(models.Model):
|
|||||||
verbose_name = "corp stats"
|
verbose_name = "corp stats"
|
||||||
verbose_name_plural = "corp stats"
|
verbose_name_plural = "corp stats"
|
||||||
|
|
||||||
objects = CorpStatsManager()
|
objects: ClassVar[CorpStatsManager] = CorpStatsManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f"{self.__class__.__name__} for {self.corp}"
|
return f"{self.__class__.__name__} for {self.corp}"
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends 'corputils/base.html' %}
|
{% extends 'corputils/base.html' %}
|
||||||
|
|
||||||
|
{% load aa_i18n %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
@ -137,7 +138,7 @@
|
|||||||
<td style="width: 30%;">{{ alt.corporation_name }}</td>
|
<td style="width: 30%;">{{ alt.corporation_name }}</td>
|
||||||
<td style="width: 30%;">{{ alt.alliance_name|default_if_none:"" }}</td>
|
<td style="width: 30%;">{{ alt.alliance_name|default_if_none:"" }}</td>
|
||||||
<td style="width: 5%;">
|
<td style="width: 5%;">
|
||||||
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge bg-danger" target="_blank">
|
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge text-bg-danger" target="_blank">
|
||||||
{% translate "Killboard" %}
|
{% translate "Killboard" %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@ -174,7 +175,7 @@
|
|||||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
||||||
<td>{{ member }}</td>
|
<td>{{ member }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ member.character_ownership.user.profile.main_character.character_name }}</td>
|
<td>{{ member.character_ownership.user.profile.main_character.character_name }}</td>
|
||||||
<td>{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
|
<td>{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
|
||||||
@ -187,7 +188,7 @@
|
|||||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||||
<td>{{ member.character_name }}</td>
|
<td>{{ member.character_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@ -218,7 +219,7 @@
|
|||||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||||
<td>{{ member.character_name }}</td>
|
<td>{{ member.character_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">
|
||||||
{% translate "Killboard" %}
|
{% translate "Killboard" %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@ -238,9 +239,12 @@
|
|||||||
{% 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] },
|
||||||
],
|
],
|
||||||
@ -249,6 +253,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#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] },
|
||||||
@ -259,6 +264,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#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] },
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "corputils/base.html" %}
|
{% extends "corputils/base.html" %}
|
||||||
|
|
||||||
|
{% load aa_i18n %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block member_data %}
|
{% block member_data %}
|
||||||
@ -27,7 +28,7 @@
|
|||||||
<td><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
<td><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
||||||
<td>{{ result.1.character_name }}</td>
|
<td>{{ result.1.character_name }}</td>
|
||||||
<td >{{ result.0.corp.corporation_name }}</td>
|
<td >{{ result.0.corp.corporation_name }}</td>
|
||||||
<td><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
<td><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
||||||
<td>{{ result.1.main_character.character_name }}</td>
|
<td>{{ result.1.main_character.character_name }}</td>
|
||||||
<td>{{ result.1.main_character.corporation_name }}</td>
|
<td>{{ result.1.main_character.corporation_name }}</td>
|
||||||
<td>{{ result.1.main_character.alliance_name }}</td>
|
<td>{{ result.1.main_character.alliance_name }}</td>
|
||||||
@ -42,9 +43,12 @@
|
|||||||
{% 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
|
||||||
});
|
});
|
||||||
|
3
allianceauth/crontab/__init__.py
Normal file
3
allianceauth/crontab/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Alliance Auth Crontab Utilities
|
||||||
|
"""
|
16
allianceauth/crontab/apps.py
Normal file
16
allianceauth/crontab/apps.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Crontab App Config
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CrontabConfig(AppConfig):
|
||||||
|
"""
|
||||||
|
Crontab App Config
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "allianceauth.crontab"
|
||||||
|
label = "crontab"
|
||||||
|
verbose_name = _("Crontab")
|
29
allianceauth/crontab/migrations/0001_initial.py
Normal file
29
allianceauth/crontab/migrations/0001_initial.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2025-01-20 06:16
|
||||||
|
|
||||||
|
import allianceauth.crontab.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CronOffset',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('minute', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Minute Offset')),
|
||||||
|
('hour', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Hour Offset')),
|
||||||
|
('day_of_month', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Day of Month Offset')),
|
||||||
|
('month_of_year', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Month of Year Offset')),
|
||||||
|
('day_of_week', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Day of Week Offset')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Cron Offsets',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
allianceauth/crontab/migrations/__init__.py
Normal file
0
allianceauth/crontab/migrations/__init__.py
Normal file
23
allianceauth/crontab/models.py
Normal file
23
allianceauth/crontab/models.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from random import random
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from solo.models import SingletonModel
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
70
allianceauth/crontab/schedulers.py
Normal file
70
allianceauth/crontab/schedulers.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django_celery_beat.schedulers import (
|
||||||
|
DatabaseScheduler
|
||||||
|
)
|
||||||
|
from django_celery_beat.models import CrontabSchedule
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
|
from celery import schedules
|
||||||
|
from celery.utils.log import get_logger
|
||||||
|
|
||||||
|
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()
|
||||||
|
except (OperationalError, ProgrammingError, ObjectDoesNotExist) as exc:
|
||||||
|
# This is just incase we haven't migrated yet or something
|
||||||
|
logger.warning(
|
||||||
|
"OffsetDatabaseScheduler: Could not fetch CronOffset (%r). "
|
||||||
|
"Defering to DatabaseScheduler",
|
||||||
|
exc
|
||||||
|
)
|
||||||
|
return super().update_from_dict(mapping)
|
||||||
|
|
||||||
|
for name, entry_fields in mapping.items():
|
||||||
|
try:
|
||||||
|
apply_offset = entry_fields.pop("apply_offset", False) # Ensure this pops before django tries to save to ORM
|
||||||
|
entry = self.Entry.from_entry(name, app=self.app, **entry_fields)
|
||||||
|
|
||||||
|
if apply_offset:
|
||||||
|
entry_fields.update({"apply_offset": apply_offset}) # Reapply this as its gets pulled from config inconsistently.
|
||||||
|
schedule_obj = entry.schedule
|
||||||
|
if isinstance(schedule_obj, schedules.crontab):
|
||||||
|
offset_cs = CrontabSchedule.from_schedule(offset_cron(schedule_obj))
|
||||||
|
offset_cs, created = CrontabSchedule.objects.get_or_create(
|
||||||
|
minute=offset_cs.minute,
|
||||||
|
hour=offset_cs.hour,
|
||||||
|
day_of_month=offset_cs.day_of_month,
|
||||||
|
month_of_year=offset_cs.month_of_year,
|
||||||
|
day_of_week=offset_cs.day_of_week,
|
||||||
|
timezone=offset_cs.timezone,
|
||||||
|
)
|
||||||
|
entry.schedule = offset_cron(schedule_obj) # This gets passed into Celery Beats Memory, important to keep it in sync with the model/DB
|
||||||
|
entry.model.crontab = offset_cs
|
||||||
|
entry.model.save()
|
||||||
|
logger.debug(f"Offset applied for '{name}' due to 'apply_offset' = True.")
|
||||||
|
|
||||||
|
if entry.model.enabled:
|
||||||
|
s[name] = entry
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error updating schedule for %s: %r", name, e)
|
||||||
|
|
||||||
|
self.schedule.update(s)
|
0
allianceauth/crontab/tests/__init__.py
Normal file
0
allianceauth/crontab/tests/__init__.py
Normal file
63
allianceauth/crontab/tests/test_models.py
Normal file
63
allianceauth/crontab/tests/test_models.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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")
|
80
allianceauth/crontab/tests/test_utils.py
Normal file
80
allianceauth/crontab/tests/test_utils.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# myapp/tests/test_tasks.py
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import ProgrammingError
|
||||||
|
from celery.schedules import crontab
|
||||||
|
|
||||||
|
from allianceauth.crontab.utils import offset_cron
|
||||||
|
from allianceauth.crontab.models import CronOffset
|
||||||
|
|
||||||
|
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)
|
50
allianceauth/crontab/utils.py
Normal file
50
allianceauth/crontab/utils.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from celery.schedules import crontab
|
||||||
|
import logging
|
||||||
|
from allianceauth.crontab.models import CronOffset
|
||||||
|
from django.db import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
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
|
3
allianceauth/custom_css/__init__.py
Normal file
3
allianceauth/custom_css/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Initializes the custom_css module.
|
||||||
|
"""
|
25
allianceauth/custom_css/admin.py
Normal file
25
allianceauth/custom_css/admin.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Admin classes for custom_css app
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Django Solos
|
||||||
|
from solo.admin import SingletonModelAdmin
|
||||||
|
|
||||||
|
# Alliance Auth Custom CSS
|
||||||
|
from allianceauth.custom_css.models import CustomCSS
|
||||||
|
from allianceauth.custom_css.forms import CustomCSSAdminForm
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CustomCSS)
|
||||||
|
class CustomCSSAdmin(SingletonModelAdmin):
|
||||||
|
"""
|
||||||
|
Custom CSS Admin
|
||||||
|
"""
|
||||||
|
|
||||||
|
form = CustomCSSAdminForm
|
||||||
|
|
||||||
|
# Leave this here for when we decide to add syntax highlighting to the CSS editor
|
||||||
|
# change_form_template = 'custom_css/admin/change_form.html'
|
13
allianceauth/custom_css/apps.py
Normal file
13
allianceauth/custom_css/apps.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
Django app configuration for custom_css
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CustomCSSConfig(AppConfig):
|
||||||
|
name = "allianceauth.custom_css"
|
||||||
|
label = "custom_css"
|
||||||
|
verbose_name = _("Custom CSS")
|
29
allianceauth/custom_css/forms.py
Normal file
29
allianceauth/custom_css/forms.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
Forms for custom_css app
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Alliance Auth Custom CSS
|
||||||
|
from allianceauth.custom_css.models import CustomCSS
|
||||||
|
from allianceauth.custom_css.widgets import CssEditorWidget
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class CustomCSSAdminForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Form for editing custom CSS
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomCSS
|
||||||
|
fields = ("css",)
|
||||||
|
widgets = {
|
||||||
|
"css": CssEditorWidget(
|
||||||
|
attrs={
|
||||||
|
"style": "width: 90%; height: 100%;",
|
||||||
|
"data-editor": "code-highlight",
|
||||||
|
"data-language": "css",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
42
allianceauth/custom_css/migrations/0001_initial.py
Normal file
42
allianceauth/custom_css/migrations/0001_initial.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 4.2.15 on 2024-08-14 11:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CustomCSS",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"css",
|
||||||
|
models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="This CSS will be added to the site after the default CSS.",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Your custom CSS",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("timestamp", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Custom CSS",
|
||||||
|
"verbose_name_plural": "Custom CSS",
|
||||||
|
"default_permissions": (),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
allianceauth/custom_css/migrations/__init__.py
Normal file
0
allianceauth/custom_css/migrations/__init__.py
Normal file
143
allianceauth/custom_css/models.py
Normal file
143
allianceauth/custom_css/models.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
Models for the custom_css app
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Django Solo
|
||||||
|
from solo.models import SingletonModel
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CustomCSS(SingletonModel):
|
||||||
|
"""
|
||||||
|
Model for storing custom CSS for the site
|
||||||
|
"""
|
||||||
|
|
||||||
|
css = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("Your custom CSS"),
|
||||||
|
help_text=_("This CSS will be added to the site after the default CSS."),
|
||||||
|
)
|
||||||
|
timestamp = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Meta for CustomCSS
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_permissions = ()
|
||||||
|
verbose_name = _("Custom CSS")
|
||||||
|
verbose_name_plural = _("Custom CSS")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""
|
||||||
|
String representation of CustomCSS
|
||||||
|
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return str(_("Custom CSS"))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Save method for CustomCSS
|
||||||
|
|
||||||
|
:param args:
|
||||||
|
:type args:
|
||||||
|
:param kwargs:
|
||||||
|
:type kwargs:
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.pk = 1
|
||||||
|
|
||||||
|
if self.css and len(self.css.replace(" ", "")) > 0:
|
||||||
|
# Write the custom CSS to a file
|
||||||
|
custom_css_file = open(
|
||||||
|
f"{settings.STATIC_ROOT}allianceauth/custom-styles.css", "w+"
|
||||||
|
)
|
||||||
|
custom_css_file.write(self.compress_css())
|
||||||
|
custom_css_file.close()
|
||||||
|
else:
|
||||||
|
# Remove the custom CSS file
|
||||||
|
try:
|
||||||
|
os.remove(f"{settings.STATIC_ROOT}allianceauth/custom-styles.css")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def compress_css(self) -> str:
|
||||||
|
"""
|
||||||
|
Compress CSS
|
||||||
|
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
css = self.css
|
||||||
|
new_css = ""
|
||||||
|
|
||||||
|
# Remove comments
|
||||||
|
css = re.sub(pattern=r"\s*/\*\s*\*/", repl="$$HACK1$$", string=css)
|
||||||
|
css = re.sub(pattern=r"/\*[\s\S]*?\*/", repl="", string=css)
|
||||||
|
css = css.replace("$$HACK1$$", "/**/")
|
||||||
|
|
||||||
|
# url() doesn't need quotes
|
||||||
|
css = re.sub(pattern=r'url\((["\'])([^)]*)\1\)', repl=r"url(\2)", string=css)
|
||||||
|
|
||||||
|
# Spaces may be safely collapsed as generated content will collapse them anyway.
|
||||||
|
css = re.sub(pattern=r"\s+", repl=" ", string=css)
|
||||||
|
|
||||||
|
# Shorten collapsable colors: #aabbcc to #abc
|
||||||
|
css = re.sub(
|
||||||
|
pattern=r"#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3(\s|;)",
|
||||||
|
repl=r"#\1\2\3\4",
|
||||||
|
string=css,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fragment values can loose zeros
|
||||||
|
css = re.sub(
|
||||||
|
pattern=r":\s*0(\.\d+([cm]m|e[mx]|in|p[ctx]))\s*;", repl=r":\1;", string=css
|
||||||
|
)
|
||||||
|
|
||||||
|
for rule in re.findall(pattern=r"([^{]+){([^}]*)}", string=css):
|
||||||
|
# We don't need spaces around operators
|
||||||
|
selectors = [
|
||||||
|
re.sub(
|
||||||
|
pattern=r"(?<=[\[\(>+=])\s+|\s+(?=[=~^$*|>+\]\)])",
|
||||||
|
repl=r"",
|
||||||
|
string=selector.strip(),
|
||||||
|
)
|
||||||
|
for selector in rule[0].split(",")
|
||||||
|
]
|
||||||
|
|
||||||
|
# Order is important, but we still want to discard repetitions
|
||||||
|
properties = {}
|
||||||
|
porder = []
|
||||||
|
|
||||||
|
for prop in re.findall(pattern="(.*?):(.*?)(;|$)", string=rule[1]):
|
||||||
|
key = prop[0].strip().lower()
|
||||||
|
|
||||||
|
if key not in porder:
|
||||||
|
porder.append(key)
|
||||||
|
|
||||||
|
properties[key] = prop[1].strip()
|
||||||
|
|
||||||
|
# output rule if it contains any declarations
|
||||||
|
if properties:
|
||||||
|
new_css += "{}{{{}}}".format(
|
||||||
|
",".join(selectors),
|
||||||
|
"".join([f"{key}:{properties[key]};" for key in porder])[:-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_css
|
@ -0,0 +1,48 @@
|
|||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
|
||||||
|
{% block field_sets %}
|
||||||
|
{% for fieldset in adminform %}
|
||||||
|
<fieldset class="module aligned {{ fieldset.classes }}">
|
||||||
|
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
|
||||||
|
|
||||||
|
{% if fieldset.description %}
|
||||||
|
<div class="description">{{ fieldset.description|safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for line in fieldset %}
|
||||||
|
<div class="form-row{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
|
||||||
|
{% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %}
|
||||||
|
|
||||||
|
{% for field in line %}
|
||||||
|
<div>
|
||||||
|
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||||
|
|
||||||
|
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% if field.is_checkbox %}
|
||||||
|
{{ field.field }}{{ field.label_tag }}
|
||||||
|
{% else %}
|
||||||
|
{{ field.label_tag }}
|
||||||
|
{% if field.is_readonly %}
|
||||||
|
<div class="readonly">{{ field.contents }}</div>
|
||||||
|
{% else %}
|
||||||
|
{{ field.field }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if field.field.help_text %}
|
||||||
|
<div class="help"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
|
||||||
|
<div>{{ field.field.help_text|safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not line.fields|length == 1 %}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block after_field_sets %}{% endblock %}
|
@ -0,0 +1,3 @@
|
|||||||
|
{% load custom_css %}
|
||||||
|
|
||||||
|
{% custom_css_static 'allianceauth/custom-styles.css' %}
|
3
allianceauth/custom_css/templatetags/__init__.py
Normal file
3
allianceauth/custom_css/templatetags/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Init file for custom_css templatetags
|
||||||
|
"""
|
48
allianceauth/custom_css/templatetags/custom_css.py
Normal file
48
allianceauth/custom_css/templatetags/custom_css.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Custom template tags for custom_css app
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Alliance Auth Custom CSS
|
||||||
|
from allianceauth.custom_css.models import CustomCSS
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template.defaulttags import register
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def custom_css_static(path: str) -> str:
|
||||||
|
"""
|
||||||
|
Versioned static URL
|
||||||
|
This is to make sure to break the browser cache on CSS updates.
|
||||||
|
|
||||||
|
Example: /static/allianceauth/custom-styles.css?v=1234567890
|
||||||
|
|
||||||
|
:param path:
|
||||||
|
:type path:
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
Path(f"{settings.STATIC_ROOT}{path}").resolve(strict=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
custom_css = CustomCSS.objects.get(pk=1)
|
||||||
|
except CustomCSS.DoesNotExist:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
custom_css_changed = custom_css.timestamp.timestamp()
|
||||||
|
custom_css_version = (
|
||||||
|
str(custom_css_changed).replace(" ", "").replace(":", "").replace("-", "")
|
||||||
|
) # remove spaces, colons, and dashes
|
||||||
|
static_url = static(path)
|
||||||
|
versioned_url = static_url + "?v=" + custom_css_version
|
||||||
|
|
||||||
|
return mark_safe(f'<link rel="stylesheet" href="{versioned_url}">')
|
38
allianceauth/custom_css/widgets.py
Normal file
38
allianceauth/custom_css/widgets.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""
|
||||||
|
Form widgets for custom_css app
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
# Alliance Auth
|
||||||
|
from allianceauth.custom_css.models import CustomCSS
|
||||||
|
|
||||||
|
|
||||||
|
class CssEditorWidget(forms.Textarea):
|
||||||
|
"""
|
||||||
|
Widget for editing CSS
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, attrs=None):
|
||||||
|
default_attrs = {"class": "custom-css-editor"}
|
||||||
|
|
||||||
|
if attrs:
|
||||||
|
default_attrs.update(attrs)
|
||||||
|
|
||||||
|
super().__init__(default_attrs)
|
||||||
|
|
||||||
|
# For when we want to add some sort of syntax highlight to it, which is not that
|
||||||
|
# easy to do on a textarea field though.
|
||||||
|
# `highlight.js` is just used as an example here, and doesn't work on a textarea field.
|
||||||
|
# class Media:
|
||||||
|
# css = {
|
||||||
|
# "all": (
|
||||||
|
# "/static/custom_css/libs/highlight.js/11.10.0/styles/github.min.css",
|
||||||
|
# )
|
||||||
|
# }
|
||||||
|
# js = (
|
||||||
|
# "/static/custom_css/libs/highlight.js/11.10.0/highlight.min.js",
|
||||||
|
# "/static/custom_css/libs/highlight.js/11.10.0/languages/css.min.js",
|
||||||
|
# "/static/custom_css/javascript/custom-css.min.js",
|
||||||
|
# )
|
@ -1,6 +1,8 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class EveonlineConfig(AppConfig):
|
class EveonlineConfig(AppConfig):
|
||||||
name = 'allianceauth.eveonline'
|
name = 'allianceauth.eveonline'
|
||||||
label = 'eveonline'
|
label = 'eveonline'
|
||||||
|
verbose_name = _('EVE Online')
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class EveAutogroupsConfig(AppConfig):
|
class EveAutogroupsConfig(AppConfig):
|
||||||
name = 'allianceauth.eveonline.autogroups'
|
name = 'allianceauth.eveonline.autogroups'
|
||||||
label = 'eve_autogroups'
|
label = 'eve_autogroups'
|
||||||
|
verbose_name = _('EVE Online Autogroups')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import allianceauth.eveonline.autogroups.signals
|
import allianceauth.eveonline.autogroups.signals
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import ClassVar
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
@ -38,13 +39,13 @@ class AutogroupsConfigManager(models.Manager):
|
|||||||
"""
|
"""
|
||||||
if state is None:
|
if state is None:
|
||||||
state = user.profile.state
|
state = user.profile.state
|
||||||
for config in self.filter(states=state):
|
|
||||||
# grant user new groups for their state
|
|
||||||
config.update_group_membership_for_user(user)
|
|
||||||
for config in self.exclude(states=state):
|
for config in self.exclude(states=state):
|
||||||
# ensure user does not have groups from previous state
|
# ensure user does not have groups from previous state
|
||||||
config.remove_user_from_alliance_groups(user)
|
config.remove_user_from_alliance_groups(user)
|
||||||
config.remove_user_from_corp_groups(user)
|
config.remove_user_from_corp_groups(user)
|
||||||
|
for config in self.filter(states=state):
|
||||||
|
# grant user new groups for their state
|
||||||
|
config.update_group_membership_for_user(user)
|
||||||
|
|
||||||
|
|
||||||
class AutogroupsConfig(models.Model):
|
class AutogroupsConfig(models.Model):
|
||||||
@ -78,7 +79,7 @@ class AutogroupsConfig(models.Model):
|
|||||||
max_length=10, default='', blank=True,
|
max_length=10, default='', blank=True,
|
||||||
help_text='Any spaces in the group name will be replaced with this.')
|
help_text='Any spaces in the group name will be replaced with this.')
|
||||||
|
|
||||||
objects = AutogroupsConfigManager()
|
objects: ClassVar[AutogroupsConfigManager] = AutogroupsConfigManager()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from allianceauth.eveonline.models import EveCorporationInfo
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
@ -73,3 +74,51 @@ class AutogroupsConfigManagerTestCase(TestCase):
|
|||||||
AutogroupsConfig.objects.update_groups_for_user(member)
|
AutogroupsConfig.objects.update_groups_for_user(member)
|
||||||
|
|
||||||
self.assertTrue(update_groups.called)
|
self.assertTrue(update_groups.called)
|
||||||
|
|
||||||
|
def test_update_group_membership_corp_in_two_configs(self):
|
||||||
|
# given
|
||||||
|
member = AuthUtils.create_member('test member')
|
||||||
|
AuthUtils.add_main_character_2(
|
||||||
|
member,
|
||||||
|
character_id='1234',
|
||||||
|
name='test character',
|
||||||
|
corp_id='2345',
|
||||||
|
corp_name='corp name',
|
||||||
|
corp_ticker='TIKK',
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
corp = EveCorporationInfo.objects.create(
|
||||||
|
corporation_id='2345',
|
||||||
|
corporation_name='corp name',
|
||||||
|
corporation_ticker='TIKK',
|
||||||
|
member_count=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
member_config = AutogroupsConfig.objects.create(corp_groups=True)
|
||||||
|
member_config.states.add(member_state)
|
||||||
|
blue_state = AuthUtils.get_blue_state()
|
||||||
|
blue_state.member_corporations.add(corp)
|
||||||
|
blue_config = AutogroupsConfig.objects.create(corp_groups=True)
|
||||||
|
blue_config.states.add(blue_state)
|
||||||
|
|
||||||
|
member.profile.state = blue_state
|
||||||
|
member.profile.save()
|
||||||
|
|
||||||
|
AutogroupsConfig.objects.update_groups_for_user(member)
|
||||||
|
|
||||||
|
# Checks before test that the role is correctly applied
|
||||||
|
group = blue_config.get_corp_group(corp)
|
||||||
|
self.assertIn(group, member.groups.all())
|
||||||
|
|
||||||
|
# when
|
||||||
|
blue_state.member_corporations.remove(corp)
|
||||||
|
member_state.member_corporations.add(corp)
|
||||||
|
member.profile.state = member_state
|
||||||
|
member.profile.save()
|
||||||
|
|
||||||
|
# then
|
||||||
|
AutogroupsConfig.objects.update_groups_for_user(member)
|
||||||
|
group = member_config.get_corp_group(corp)
|
||||||
|
self.assertIn(group, member.groups.all())
|
||||||
|
@ -10,7 +10,7 @@ from . import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_BASE_URL = 'http://evemaps.dotlan.net'
|
_BASE_URL = 'https://evemaps.dotlan.net'
|
||||||
|
|
||||||
|
|
||||||
def _build_url(category: str, name: str) -> str:
|
def _build_url(category: str, name: str) -> str:
|
||||||
|
@ -31,29 +31,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'),
|
||||||
'http://evemaps.dotlan.net/alliance/Wayne_Enterprices'
|
'https://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'),
|
||||||
'http://evemaps.dotlan.net/corp/Wayne_Technology'
|
'https://evemaps.dotlan.net/corp/Wayne_Technology'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
dotlan.corporation_url('Crédit Agricole'),
|
dotlan.corporation_url('Crédit Agricole'),
|
||||||
'http://evemaps.dotlan.net/corp/Cr%C3%A9dit_Agricole'
|
'https://evemaps.dotlan.net/corp/Cr%C3%A9dit_Agricole'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_region_url(self):
|
def test_region_url(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
dotlan.region_url('Black Rise'),
|
dotlan.region_url('Black Rise'),
|
||||||
'http://evemaps.dotlan.net/map/Black_Rise'
|
'https://evemaps.dotlan.net/map/Black_Rise'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_solar_system_url(self):
|
def test_solar_system_url(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
dotlan.solar_system_url('Jita'),
|
dotlan.solar_system_url('Jita'),
|
||||||
'http://evemaps.dotlan.net/system/Jita'
|
'https://evemaps.dotlan.net/system/Jita'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,10 +14,20 @@ class EveCharacterProviderManager:
|
|||||||
class EveCharacterManager(models.Manager):
|
class EveCharacterManager(models.Manager):
|
||||||
provider = EveCharacterProviderManager()
|
provider = EveCharacterProviderManager()
|
||||||
|
|
||||||
def create_character(self, character_id):
|
def exclude_biomassed(self):
|
||||||
|
"""
|
||||||
|
Get a queryset of EveCharacter objects, excluding the "Doomheim" corporation (1000001).
|
||||||
|
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.exclude(corporation_id=1000001)
|
||||||
|
|
||||||
|
def create_character(self, character_id) -> models.Model:
|
||||||
return self.create_character_obj(self.provider.get_character(character_id))
|
return self.create_character_obj(self.provider.get_character(character_id))
|
||||||
|
|
||||||
def create_character_obj(self, character: providers.Character):
|
def create_character_obj(self, character: providers.Character) -> models.Model:
|
||||||
return self.create(
|
return self.create(
|
||||||
character_id=character.id,
|
character_id=character.id,
|
||||||
character_name=character.name,
|
character_name=character.name,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Union
|
from typing import ClassVar, Union
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -75,8 +75,8 @@ class EveAllianceInfo(models.Model):
|
|||||||
alliance_ticker = models.CharField(max_length=254)
|
alliance_ticker = models.CharField(max_length=254)
|
||||||
executor_corp_id = models.PositiveIntegerField()
|
executor_corp_id = models.PositiveIntegerField()
|
||||||
|
|
||||||
objects = EveAllianceManager()
|
objects: ClassVar[EveAllianceManager] = EveAllianceManager()
|
||||||
provider = EveAllianceProviderManager()
|
provider: ClassVar[EveAllianceProviderManager] = EveAllianceProviderManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=['executor_corp_id',])]
|
indexes = [models.Index(fields=['executor_corp_id',])]
|
||||||
@ -147,7 +147,7 @@ class EveCorporationInfo(models.Model):
|
|||||||
EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL
|
EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = EveCorporationManager()
|
objects: ClassVar[EveCorporationManager] = EveCorporationManager()
|
||||||
provider = EveCorporationProviderManager()
|
provider = EveCorporationProviderManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -214,7 +214,7 @@ class EveCharacter(models.Model):
|
|||||||
faction_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
faction_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
||||||
faction_name = models.CharField(max_length=254, blank=True, null=True, default='')
|
faction_name = models.CharField(max_length=254, blank=True, null=True, default='')
|
||||||
|
|
||||||
objects = EveCharacterManager()
|
objects: ClassVar[EveCharacterManager] = EveCharacterManager()
|
||||||
provider = EveCharacterProviderManager()
|
provider = EveCharacterProviderManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from random import randint
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
|
||||||
@ -9,7 +10,8 @@ from . import providers
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
TASK_PRIORITY = 7
|
TASK_PRIORITY = 7
|
||||||
CHUNK_SIZE = 500
|
CHARACTER_AFFILIATION_CHUNK_SIZE = 500
|
||||||
|
EVEONLINE_TASK_JITTER = 600
|
||||||
|
|
||||||
|
|
||||||
def chunks(lst, n):
|
def chunks(lst, n):
|
||||||
@ -19,13 +21,13 @@ def chunks(lst, n):
|
|||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def update_corp(corp_id):
|
def update_corp(corp_id: int) -> None:
|
||||||
"""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):
|
def update_alliance(alliance_id: int) -> None:
|
||||||
"""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()
|
||||||
|
|
||||||
@ -37,23 +39,30 @@ def update_character(character_id: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def run_model_update():
|
def run_model_update() -> None:
|
||||||
"""Update all alliances, corporations and characters from ESI"""
|
"""Update all alliances, corporations and characters from ESI"""
|
||||||
|
|
||||||
#update existing corp models
|
# Queue update tasks for Known Corporation Models
|
||||||
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
||||||
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
|
update_corp.apply_async(
|
||||||
|
args=[corp['corporation_id']],
|
||||||
|
priority=TASK_PRIORITY,
|
||||||
|
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||||
|
|
||||||
# update existing alliance models
|
# Queue update tasks for Known Alliance Models
|
||||||
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
|
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
|
||||||
update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
|
update_alliance.apply_async(
|
||||||
|
args=[alliance['alliance_id']],
|
||||||
|
priority=TASK_PRIORITY,
|
||||||
|
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||||
|
|
||||||
# update existing character models
|
# Queue update tasks for Known Character Models
|
||||||
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
|
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
|
||||||
for character_ids_chunk in chunks(character_ids, CHUNK_SIZE):
|
for character_ids_chunk in chunks(character_ids, CHARACTER_AFFILIATION_CHUNK_SIZE):
|
||||||
update_character_chunk.apply_async(
|
update_character_chunk.apply_async(
|
||||||
args=[character_ids_chunk], priority=TASK_PRIORITY
|
args=[character_ids_chunk],
|
||||||
)
|
priority=TASK_PRIORITY,
|
||||||
|
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
@ -68,8 +77,9 @@ 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], priority=TASK_PRIORITY
|
args=[character_id],
|
||||||
)
|
priority=TASK_PRIORITY,
|
||||||
|
countdown=randint(1, EVEONLINE_TASK_JITTER))
|
||||||
return
|
return
|
||||||
|
|
||||||
affiliations = {
|
affiliations = {
|
||||||
@ -107,5 +117,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')], priority=TASK_PRIORITY
|
args=[character.get('character_id')],
|
||||||
)
|
priority=TASK_PRIORITY)
|
||||||
|
@ -723,5 +723,5 @@ class TestEveSwaggerProvider(TestCase):
|
|||||||
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.assertEqual(
|
self.assertEqual(
|
||||||
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0'
|
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 dummy@example.net'
|
||||||
)
|
)
|
||||||
|
@ -84,7 +84,7 @@ class TestUpdateTasks(TestCase):
|
|||||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
@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.CHUNK_SIZE', 2)
|
@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_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.CHUNK_SIZE', 2)
|
@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_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:
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class FatConfig(AppConfig):
|
class FatConfig(AppConfig):
|
||||||
name = 'allianceauth.fleetactivitytracking'
|
name = 'allianceauth.fleetactivitytracking'
|
||||||
label = 'fleetactivitytracking'
|
label = 'fleetactivitytracking'
|
||||||
|
verbose_name = _('Fleet Activity Tracking')
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<th class="text-center">{% translate "Character" %}</th>
|
<th class="text-center">{% translate "Character" %}</th>
|
||||||
<th class="text-center">{% translate "System" %}</th>
|
<th class="text-center">{% translate "System" %}</th>
|
||||||
<th class="text-center">{% translate "Ship" %}</th>
|
<th class="text-center">{% translate "Ship" %}</th>
|
||||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
<th class="text-center">{% translate "EVE time" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="text-center">{% translate "Fleet" %}</th>
|
<th class="text-center">{% translate "Fleet" %}</th>
|
||||||
<th class="text-center">{% translate "Creator" %}</th>
|
<th class="text-center">{% translate "Creator" %}</th>
|
||||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
<th class="text-center">{% translate "EVE time" %}</th>
|
||||||
<th class="text-center">{% translate "Duration" %}</th>
|
<th class="text-center">{% translate "Duration" %}</th>
|
||||||
<th class="text-center">{% translate "Edit" %}</th>
|
<th class="text-center">{% translate "Edit" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -80,7 +80,7 @@
|
|||||||
{% for link in created_fats %}
|
{% for link in created_fats %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">
|
<a href="{% url 'fatlink:click' link.hash %}" class="badge text-bg-primary">
|
||||||
{{ link.fleet }}
|
{{ link.fleet }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<th scope="col" class="text-center">{% translate "Character" %}</th>
|
<th scope="col" class="text-center">{% translate "Character" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "System" %}</th>
|
<th scope="col" class="text-center">{% translate "System" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Ship" %}</th>
|
<th scope="col" class="text-center">{% translate "Ship" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
|
<th scope="col" class="text-center">{% translate "EVE time" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% for fat in fats %}
|
{% for fat in fats %}
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<th scope="col" class="text-center">{% translate "Name" %}</th>
|
<th scope="col" class="text-center">{% translate "Name" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Creator" %}</th>
|
<th scope="col" class="text-center">{% translate "Creator" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
|
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
|
<th scope="col" class="text-center">{% translate "EVE time" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Duration" %}</th>
|
<th scope="col" class="text-center">{% translate "Duration" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Edit" %}</th>
|
<th scope="col" class="text-center">{% translate "Edit" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -97,7 +97,7 @@
|
|||||||
{% for link in fatlinks %}
|
{% for link in fatlinks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a>
|
<a href="{% url 'fatlink:click' link.hash %}" class="badge text-bg-primary">{{ link.fleet }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ link.creator.username }}</td>
|
<td class="text-center">{{ link.creator.username }}</td>
|
||||||
<td class="text-center">{{ link.fleet }}</td>
|
<td class="text-center">{{ link.fleet }}</td>
|
||||||
|
@ -10,23 +10,38 @@ from allianceauth.authentication.models import CharacterOwnership
|
|||||||
from allianceauth.eveonline.models import EveCharacter
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
|
|
||||||
|
|
||||||
def get_all_characters_from_user(user: User) -> list:
|
def get_all_characters_from_user(user: User, main_first: bool = False) -> list:
|
||||||
"""
|
"""
|
||||||
Get all characters from a user or an empty list
|
Get all characters from a user
|
||||||
when no characters are found for the user or the user is None
|
This function retrieves all characters associated with a given user, optionally ordering them
|
||||||
|
with the main character first.
|
||||||
|
If the user is None, an empty list is returned.
|
||||||
|
|
||||||
:param user:
|
:param user: The user whose characters are to be retrieved
|
||||||
:type user:
|
:type user: User
|
||||||
:return:
|
:param main_first: If True, the main character will be listed first
|
||||||
:rtype:
|
:type main_first: bool
|
||||||
|
:return: A list of EveCharacter objects associated with the user
|
||||||
|
:rtype: list[EveCharacter]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if main_first:
|
||||||
characters = [
|
characters = [
|
||||||
char.character for char in CharacterOwnership.objects.filter(user=user)
|
char.character
|
||||||
|
for char in CharacterOwnership.objects.filter(user=user).order_by(
|
||||||
|
"-character__userprofile", "character__character_name"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
characters = [
|
||||||
|
char.character
|
||||||
|
for char in CharacterOwnership.objects.filter(user=user).order_by(
|
||||||
|
"character__character_name"
|
||||||
|
)
|
||||||
]
|
]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return []
|
return []
|
||||||
|
@ -3,6 +3,7 @@ Framework App Config
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class FrameworkConfig(AppConfig):
|
class FrameworkConfig(AppConfig):
|
||||||
@ -12,3 +13,4 @@ class FrameworkConfig(AppConfig):
|
|||||||
|
|
||||||
name = "allianceauth.framework"
|
name = "allianceauth.framework"
|
||||||
label = "framework"
|
label = "framework"
|
||||||
|
verbose_name = _("Framework")
|
||||||
|
@ -5,6 +5,24 @@
|
|||||||
* to be used throughout Alliance Auth and its Community Apps
|
* to be used throughout Alliance Auth and its Community Apps
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* General
|
||||||
|
------------------------------------------------------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.navbar-toggler.collapsed {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#nav-right:has(li) + ul#nav-right-character-control > li:first-child {
|
||||||
|
display: list-item !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 991px) {
|
||||||
|
ul#nav-left:has(li) + ul#nav-right + ul#nav-right-character-control > li:first-child {
|
||||||
|
display: list-item !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Bootstrap fixes
|
/* Bootstrap fixes
|
||||||
------------------------------------------------------------------------------------- */
|
------------------------------------------------------------------------------------- */
|
||||||
@media all {
|
@media all {
|
||||||
@ -13,6 +31,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Image overflow fix
|
||||||
|
------------------------------------------------------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Side Navigation
|
/* Side Navigation
|
||||||
------------------------------------------------------------------------------------- */
|
------------------------------------------------------------------------------------- */
|
||||||
@media all {
|
@media all {
|
||||||
@ -40,14 +67,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Chevron icons */
|
/* Chevron icons */
|
||||||
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-down,
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-down,
|
||||||
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-right {
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-right {
|
||||||
display: block;
|
display: block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-right,
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-right,
|
||||||
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-down {
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-down {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
105
allianceauth/framework/staticfiles/storage.py
Normal file
105
allianceauth/framework/staticfiles/storage.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
"""
|
||||||
|
Custom static files storage for Alliance Auth.
|
||||||
|
|
||||||
|
This module defines a custom static files storage class for
|
||||||
|
Alliance Auth, named `AaManifestStaticFilesStorage`.
|
||||||
|
|
||||||
|
Using `ManifestStaticFilesStorage` will give us a hashed name for
|
||||||
|
our static files, which is useful for cache busting.
|
||||||
|
|
||||||
|
This storage class extends Django's `ManifestStaticFilesStorage` to ignore missing files,
|
||||||
|
which the original class does not handle, and log them in debug mode.
|
||||||
|
It is useful for handling cases where static files may not exist, such as when a
|
||||||
|
CSS file references a background image that is not present in the static files directory.
|
||||||
|
|
||||||
|
With debug mode enabled, it will print a message for each missing file when running `collectstatic`,
|
||||||
|
which can help identify issues with static file references during development.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
|
||||||
|
|
||||||
|
|
||||||
|
class AaManifestStaticFilesStorage(ManifestStaticFilesStorage):
|
||||||
|
"""
|
||||||
|
Custom static files storage that ignores missing files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cleanup_name(cls, name: str) -> str:
|
||||||
|
"""
|
||||||
|
Clean up the name by removing quotes.
|
||||||
|
This method is used to ensure that the name does not contain any quotes,
|
||||||
|
which can cause issues with file paths.
|
||||||
|
|
||||||
|
:param name: The name of the static file.
|
||||||
|
:type name: str
|
||||||
|
:return: The cleaned-up name without quotes.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Remove quotes from the name
|
||||||
|
return name.replace('"', "").replace("'", "")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize the static files storage, ignoring missing files.
|
||||||
|
|
||||||
|
:param args:
|
||||||
|
:type args:
|
||||||
|
:param kwargs:
|
||||||
|
:type kwargs:
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.missing_files = []
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def hashed_name(self, name, content=None, filename=None):
|
||||||
|
"""
|
||||||
|
Generate a hashed name for the given static file, ignoring missing files.
|
||||||
|
|
||||||
|
Ignore missing files, e.g. non-existent background image referenced from css.
|
||||||
|
Returns the original filename if the referenced file doesn't exist.
|
||||||
|
|
||||||
|
:param name: The name of the static file to hash.
|
||||||
|
:type name: str
|
||||||
|
:param content: The content of the static file, if available.
|
||||||
|
:type content: bytes | None
|
||||||
|
:param filename: The original filename of the static file, if available.
|
||||||
|
:type filename: str | None
|
||||||
|
:return: The hashed name of the static file, or the original name if the file is missing.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
clean_name = self._cleanup_name(name)
|
||||||
|
|
||||||
|
return super().hashed_name(clean_name, content, filename)
|
||||||
|
except ValueError as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
# In debug mode, we log the missing file message
|
||||||
|
message = e.args[0].split(" with ")[0]
|
||||||
|
self.missing_files.append(message)
|
||||||
|
# print(f'\x1b[0;30;41m{message}\x1b[0m')
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
def post_process(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Post-process the static files, printing any missing files in debug mode.
|
||||||
|
|
||||||
|
:param args:
|
||||||
|
:type args:
|
||||||
|
:param kwargs:
|
||||||
|
:type kwargs:
|
||||||
|
:return:
|
||||||
|
:rtype:
|
||||||
|
"""
|
||||||
|
|
||||||
|
yield from super().post_process(*args, **kwargs)
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
# In debug mode, print the missing files
|
||||||
|
for message in sorted(set(self.missing_files)):
|
||||||
|
print(f"\x1b[0;30;41m{message}\x1b[0m")
|
@ -2,7 +2,8 @@
|
|||||||
{# {% include "framework/header/page-header.html" with title="Foobar" subtitle="Barfoo" %}#}
|
{# {% include "framework/header/page-header.html" with title="Foobar" subtitle="Barfoo" %}#}
|
||||||
|
|
||||||
{% if title %}
|
{% if title %}
|
||||||
<h1 class="page-header text-center mb-3">
|
<header class="aa-page-header mb-3">
|
||||||
|
<h1 class="page-header text-center">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
||||||
{% if subtitle %}
|
{% if subtitle %}
|
||||||
@ -10,4 +11,5 @@
|
|||||||
<small class="text-muted">{{ subtitle }}</small>
|
<small class="text-muted">{{ subtitle }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
</header>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class GroupManagementConfig(AppConfig):
|
class GroupManagementConfig(AppConfig):
|
||||||
name = 'allianceauth.groupmanagement'
|
name = 'allianceauth.groupmanagement'
|
||||||
label = 'groupmanagement'
|
label = 'groupmanagement'
|
||||||
verbose_name = 'Group Management'
|
verbose_name = _('Group Management')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals # noqa: F401
|
from . import signals # noqa: F401
|
||||||
|
@ -49,7 +49,7 @@ class GroupsMenuItem(MenuItemHook):
|
|||||||
MenuItemHook.__init__(
|
MenuItemHook.__init__(
|
||||||
self,
|
self,
|
||||||
text=_("Groups"),
|
text=_("Groups"),
|
||||||
classes="fa-solid fa-user",
|
classes="fa-solid fa-users",
|
||||||
url_name="groupmanagement:groups",
|
url_name="groupmanagement:groups",
|
||||||
order=25,
|
order=25,
|
||||||
navactive=[
|
navactive=[
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% 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 %}
|
||||||
@ -73,6 +74,8 @@
|
|||||||
{% 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;
|
||||||
@ -94,6 +97,7 @@
|
|||||||
$.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:
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% 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 %}
|
||||||
@ -86,9 +87,12 @@
|
|||||||
{% 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: [
|
||||||
{
|
{
|
||||||
|
@ -39,12 +39,12 @@
|
|||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if group.authgroup.hidden %}
|
{% if group.authgroup.hidden %}
|
||||||
<span class="badge bg-info">{% translate "Hidden" %}</span>
|
<span class="badge text-bg-info">{% translate "Hidden" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if group.authgroup.open %}
|
{% if group.authgroup.open %}
|
||||||
<span class="badge bg-success">{% translate "Open" %}</span>
|
<span class="badge text-bg-success">{% translate "Open" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">{% translate "Requestable" %}</span>
|
<span class="badge text-bg-secondary">{% translate "Requestable" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% 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 %}
|
||||||
@ -16,7 +17,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}
|
<a class="nav-link" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}
|
||||||
{% if req_count %}
|
{% if req_count %}
|
||||||
<span class="badge bg-secondary">{{ req_count }}</span>
|
<span class="badge text-bg-secondary">{{ req_count }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -32,8 +33,8 @@
|
|||||||
<th>{% translate "Description" %}</th>
|
<th>{% translate "Description" %}</th>
|
||||||
<th>
|
<th>
|
||||||
{% translate "Leaders" %}<br>
|
{% translate "Leaders" %}<br>
|
||||||
<span class="my-1 me-1 fw-lighter badge bg-primary">{% translate "User" %}</span>
|
<span class="my-1 me-1 fw-lighter badge text-bg-primary">{% translate "User" %}</span>
|
||||||
<span class="my-1 me-1 fw-lighter badge bg-secondary">{% translate "Group" %}</span>
|
<span class="my-1 me-1 fw-lighter badge text-bg-secondary">{% translate "Group" %}</span>
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -52,13 +53,13 @@
|
|||||||
{% if g.group.authgroup.group_leaders.all.count %}
|
{% if g.group.authgroup.group_leaders.all.count %}
|
||||||
{% for leader in g.group.authgroup.group_leaders.all %}
|
{% for leader in g.group.authgroup.group_leaders.all %}
|
||||||
{% if leader.profile.main_character %}
|
{% if leader.profile.main_character %}
|
||||||
<span class="my-1 me-1 badge bg-primary">{{leader.profile.main_character}}</span>
|
<span class="my-1 me-1 badge text-bg-primary">{{leader.profile.main_character}}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.group.authgroup.group_leaders.all.count %}
|
{% if g.group.authgroup.group_leader_groups.all.count %}
|
||||||
{% for group in g.group.authgroup.group_leader_groups.all %}
|
{% 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 text-bg-secondary">{{group.name}}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@ -103,9 +104,13 @@
|
|||||||
{% 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 %}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
{% translate "Join Requests" %}
|
{% translate "Join Requests" %}
|
||||||
|
|
||||||
{% if acceptrequests %}
|
{% if acceptrequests %}
|
||||||
<span class="badge bg-secondary">{{ acceptrequests|length }}</span>
|
<span class="badge text-bg-secondary">{{ acceptrequests|length }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
{% translate "Leave Requests" %}
|
{% translate "Leave Requests" %}
|
||||||
|
|
||||||
{% if leaverequests %}
|
{% if leaverequests %}
|
||||||
<span class="badge bg-secondary">{{ leaverequests|length }}</span>
|
<span class="badge text-bg-secondary">{{ leaverequests|length }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class HRApplicationsConfig(AppConfig):
|
class HRApplicationsConfig(AppConfig):
|
||||||
name = 'allianceauth.hrapplications'
|
name = 'allianceauth.hrapplications'
|
||||||
label = 'hrapplications'
|
label = 'hrapplications'
|
||||||
|
verbose_name = _('HR Applications')
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from typing import ClassVar
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
@ -40,7 +41,7 @@ class Application(models.Model):
|
|||||||
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
objects = ApplicationManager()
|
objects: ClassVar[ApplicationManager] = ApplicationManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user) + " Application To " + str(self.form)
|
return str(self.user) + " Application To " + str(self.form)
|
||||||
|
@ -43,11 +43,11 @@
|
|||||||
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
|
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if personal_app.approved == None %}
|
{% if personal_app.approved == None %}
|
||||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
||||||
{% elif personal_app.approved == True %}
|
{% elif personal_app.approved == True %}
|
||||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@ -133,14 +133,14 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if app.approved == None %}
|
{% if app.approved == None %}
|
||||||
{% if app.reviewer_str %}
|
{% if app.reviewer_str %}
|
||||||
<div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
<div class="badge text-bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif app.approved == True %}
|
{% elif app.approved == True %}
|
||||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@ -177,14 +177,14 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if app.approved == None %}
|
{% if app.approved == None %}
|
||||||
{% if app.reviewer_str %}
|
{% if app.reviewer_str %}
|
||||||
<div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
<div class="badge text-bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif app.approved == True %}
|
{% elif app.approved == True %}
|
||||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
|
@ -43,11 +43,11 @@
|
|||||||
<td>{{ app.form.corp }}</td>
|
<td>{{ app.form.corp }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if app.approved == None %}
|
{% if app.approved == None %}
|
||||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
||||||
{% elif app.approved == True %}
|
{% elif app.approved == True %}
|
||||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header bg-info">
|
<div class="card-header text-bg-info">
|
||||||
<div class="card-title mb-0">{% translate "Applicant" %}</div>
|
<div class="card-title mb-0">{% translate "Applicant" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header bg-info">
|
<div class="card-header text-bg-info">
|
||||||
<div class="card-title mb-0">{% translate "Characters" %}</div>
|
<div class="card-title mb-0">{% translate "Characters" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Binary file not shown.
BIN
allianceauth/locale/cs_CZ/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/cs_CZ/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
allianceauth/locale/nl_NL/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/nl_NL/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user