Compare commits

..

32 Commits

Author SHA1 Message Date
Joel Falknau
bd17b95cac Version Bump 4.3.0 2024-09-09 14:05:58 +10:00
Joel Falknau
4ed1c5b7c4 bring test celery in line with project 2024-09-09 13:59:43 +10:00
Ariel Rin
271fd8e8c4 Merge branch 'docs-user-creation-fix' into 'master'
[Docs] Add shell option to adduser command for Ubuntu install docs

See merge request allianceauth/allianceauth!1622
2024-09-09 03:58:47 +00:00
colcrunch
9b4321281a [Docs] Add shell option to adduser command for Ubuntu install docs 2024-09-09 03:58:47 +00:00
Ariel Rin
052c35c8e5 Merge branch 'metenox' into 'master'
Timerboard Improvements

Closes #1384

See merge request allianceauth/allianceauth!1645
2024-09-09 03:57:21 +00:00
Ariel Rin
0fcb517b0b Timerboard Improvements 2024-09-09 03:57:21 +00:00
Ariel Rin
dcfddf0add Merge branch 'docs2' into 'master'
Documentation

See merge request allianceauth/allianceauth!1646
2024-09-09 03:43:30 +00:00
Joel Falknau
4a4258d0e6 use docker command inside docker 2024-09-09 13:32:18 +10:00
Joel Falknau
dd15a221aa split repo from install 2024-09-09 13:32:06 +10:00
Joel Falknau
737e02293a expand mumble service install docs 2024-09-09 12:59:49 +10:00
Joel Falknau
c34efebacf define charsets for db backed apps 2024-09-03 12:27:50 +10:00
Joel Falknau
4602097399 remove DB step from mumble install, sqlite has been reccommended for a while 2024-09-03 12:27:02 +10:00
Joel Falknau
7051e06564 update features from readme 2024-09-03 12:26:43 +10:00
Joel Falknau
9767ce79d8 cleanup some local.py references 2024-09-03 12:26:31 +10:00
Joel Falknau
0c090f1486 Ubuntu 2004 should not be reccommended for new isntalls 2024-09-03 12:25:01 +10:00
Joel Falknau
618ee81f9b remove unneeded analytics docs for removed features 2024-09-03 12:24:37 +10:00
Joel Falknau
98efb9f887 split maintenance tasks into bare metal / docker 2024-09-03 12:24:17 +10:00
Ariel Rin
cbe6c821cc Merge branch 'custom-css' into 'master'
[ADD] Custom CSS Module

See merge request allianceauth/allianceauth!1643
2024-08-21 05:01:38 +00:00
Ariel Rin
de9d2b39a6 Merge branch 'theme-html-tags' into 'master'
[ADD] Theme html tags

See merge request allianceauth/allianceauth!1642
2024-08-21 04:59:18 +00:00
Peter Pfeufer
0d5f22288b Merge branch 'switch-to-django-solo' into custom-css 2024-08-20 14:41:52 +02:00
Peter Pfeufer
e0d76dc268 [CHANGE] Switch to Django Solo 2024-08-20 14:41:43 +02:00
Peter Pfeufer
ecc9e68330 [CHANGE] Consolidate migrations 2024-08-14 13:25:49 +02:00
Peter Pfeufer
710149ec21 [FIX] Check if the CustomCSS object exists 2024-08-14 13:22:23 +02:00
Peter Pfeufer
3c2c137dad [CHANGE] improve try block in template tag 2024-08-14 13:05:32 +02:00
Peter Pfeufer
a8271c4189 [CHANGE] Remove custom CSS file when it will be empty 2024-08-14 12:58:01 +02:00
Peter Pfeufer
3315ae7778 [ADD] Module to base settings file 2024-08-14 12:47:38 +02:00
Peter Pfeufer
d2f048f8fe [ADD Example template for admin overrides
For when we might want to add syntax highlight ti it, which is a completely different can of worms though.
2024-08-14 12:45:57 +02:00
Peter Pfeufer
0fe2855faa [ADD] Custom CSS to base file
Check if the CSS file exists and add it to the HTML output
2024-08-14 12:44:51 +02:00
Peter Pfeufer
79a1fa3d7c [ADD] CSS compression on save 2024-08-14 12:42:52 +02:00
Peter Pfeufer
96fe88d5c7 [REMOVE] highlight.js and leave it as an example in the widget code 2024-08-14 11:53:02 +02:00
Peter Pfeufer
59391ad3c5 [ADD] Custom CSS module (First steps) 2024-08-11 22:34:16 +02:00
Peter Pfeufer
94e9c08422 [ADD] Theme html tags 2024-08-08 10:22:14 +02:00
92 changed files with 1391 additions and 625 deletions

View File

@@ -51,6 +51,30 @@ secret_detection:
stage: gitlab stage: gitlab
before_script: [] before_script: []
test-3.8-core:
<<: *only-default
image: python:3.8-bookworm
script:
- tox -e py38-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.9-core:
<<: *only-default
image: python:3.9-bookworm
script:
- tox -e py39-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.10-core: test-3.10-core:
<<: *only-default <<: *only-default
image: python:3.10-bookworm image: python:3.10-bookworm
@@ -87,18 +111,29 @@ test-3.12-core:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
test-3.13-core: test-3.8-all:
<<: *only-default <<: *only-default
image: python:3.13-rc-bookworm image: python:3.8-bookworm
script: script:
- tox -e py313-core - tox -e py38-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.9-all:
<<: *only-default
image: python:3.9-bookworm
script:
- tox -e py39-all
artifacts: artifacts:
when: always when: always
reports: reports:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
allow_failure: true
test-3.10-all: test-3.10-all:
<<: *only-default <<: *only-default
@@ -137,22 +172,9 @@ test-3.12-all:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
test-3.13-all:
<<: *only-default
image: python:3.13-rc-bookworm
script:
- tox -e py313-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
build-test: build-test:
stage: test stage: test
image: python:3.12-bookworm image: python:3.11-bookworm
before_script: before_script:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
@@ -171,13 +193,13 @@ build-test:
test-docs: test-docs:
<<: *only-default <<: *only-default
image: python:3.12-bookworm image: python:3.11-bookworm
script: script:
- tox -e docs - tox -e docs
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.12-bookworm image: python:3.11-bookworm
before_script: before_script:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
@@ -193,10 +215,10 @@ deploy_production:
build-image: build-image:
before_script: [] before_script: []
image: docker:27.0 image: docker:24.0
stage: docker stage: docker
services: services:
- docker:27-dind - docker:24.0-dind
script: | script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
@@ -217,10 +239,10 @@ build-image:
build-image-dev: build-image-dev:
before_script: [] before_script: []
image: docker:27 image: docker:24.0
stage: docker stage: docker
services: services:
- docker:27-dind - docker:24.0-dind
script: | script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
@@ -238,10 +260,10 @@ build-image-dev:
build-image-mr: build-image-mr:
before_script: [] before_script: []
image: docker:27 image: docker:24.0
stage: docker stage: docker
services: services:
- docker:27-dind - docker:24.0-dind
script: | script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA

View File

@@ -9,7 +9,7 @@ repos:
rev: v3.15.2 rev: v3.15.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py310-plus] args: [--py38-plus]
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.17.0 rev: 1.17.0
hooks: hooks:

View File

@@ -7,11 +7,11 @@ version: 2
# Set the version of Python and other tools you might need # Set the version of Python and other tools you might need
build: build:
os: ubuntu-24.04 os: ubuntu-22.04
apt_packages: apt_packages:
- redis - redis
tools: tools:
python: "3.12" python: "3.11"
jobs: jobs:
post_system_dependencies: post_system_dependencies:
- redis-server --daemonize yes - redis-server --daemonize yes

View File

@@ -5,7 +5,7 @@ manage online service access.
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '4.2.2' __version__ = '4.3.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__}'

View File

@@ -12,7 +12,7 @@ from django.conf import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def active_tasks_count() -> int | None: def active_tasks_count() -> Optional[int]:
"""Return count of currently active tasks """Return count of currently active tasks
or None if celery workers are not online. or None if celery workers are not online.
""" """
@@ -20,7 +20,7 @@ def active_tasks_count() -> int | None:
return _tasks_count(inspect.active()) return _tasks_count(inspect.active())
def _tasks_count(data: dict) -> int | None: def _tasks_count(data: dict) -> Optional[int]:
"""Return count of tasks in data from celery inspect API.""" """Return count of tasks in data from celery inspect API."""
try: try:
tasks = itertools.chain(*data.values()) tasks = itertools.chain(*data.values())
@@ -29,7 +29,7 @@ def _tasks_count(data: dict) -> int | None:
return len(list(tasks)) return len(list(tasks))
def queued_tasks_count() -> int | None: def queued_tasks_count() -> Optional[int]:
"""Return count of queued tasks. Return None if there was an error.""" """Return count of queued tasks. Return None if there was an error."""
try: try:
with current_app.connection_or_acquire() as conn: with current_app.connection_or_acquire() as conn:

View File

@@ -2,8 +2,7 @@ from django.urls import include
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from functools import wraps from functools import wraps
from typing import Optional from typing import Callable, Iterable, Optional
from collections.abc import Callable, Iterable
from django.urls import include from django.urls import include
from django.contrib import messages from django.contrib import messages
@@ -18,7 +17,7 @@ def user_has_main_character(user):
def decorate_url_patterns( def decorate_url_patterns(
urls, decorator: Callable, excluded_views: Iterable | None = None urls, decorator: Callable, excluded_views: Optional[Iterable] = None
): ):
"""Decorate views given in url patterns except when they are explicitly excluded. """Decorate views given in url patterns except when they are explicitly excluded.

View File

@@ -16,7 +16,7 @@ class _TaskCounts(NamedTuple):
retried: int retried: int
failed: int failed: int
total: int total: int
earliest_task: dt.datetime | None earliest_task: Optional[dt.datetime]
hours: int hours: int

View File

@@ -17,7 +17,7 @@ class EventSeries:
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES" _ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis | None = None) -> None: def __init__(self, key_id: str, redis: Optional[Redis] = None) -> None:
self._redis = get_redis_client_or_stub() if not redis else redis self._redis = get_redis_client_or_stub() if not redis else redis
self._key_id = str(key_id) self._key_id = str(key_id)
self.clear() self.clear()
@@ -46,7 +46,7 @@ class EventSeries:
my_id = self._redis.incr(self._key_counter) my_id = self._redis.incr(self._key_counter)
self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()}) self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})
def all(self) -> list[dt.datetime]: def all(self) -> List[dt.datetime]:
"""List of all known events.""" """List of all known events."""
return [ return [
event[1] event[1]
@@ -75,7 +75,7 @@ class EventSeries:
maximum = "+inf" if not latest else latest.timestamp() maximum = "+inf" if not latest else latest.timestamp()
return self._redis.zcount(self._key_sorted_set, min=minimum, max=maximum) return self._redis.zcount(self._key_sorted_set, min=minimum, max=maximum)
def first_event(self, earliest: dt.datetime = None) -> dt.datetime | None: def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
"""Date/Time of first event. Returns `None` if series has no events. """Date/Time of first event. Returns `None` if series has no events.
Args: Args:

View File

@@ -1,3 +1,4 @@
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
from allianceauth.utils.cache import get_redis_client from allianceauth.utils.cache import get_redis_client
@@ -6,7 +7,7 @@ from packaging.version import InvalidVersion, Version as Pep440Version
from celery import current_app from celery import current_app
from django.conf import settings from django.conf import settings
from sqlite3.dbapi2 import sqlite_version_info from sqlite3.dbapi2 import sqlite_version_info
import datetime
""" """
A = System Packages A = System Packages
B = Configuration B = Configuration
@@ -14,8 +15,8 @@ B = Configuration
@register() @register()
def django_settings(app_configs, **kwargs) -> list[CheckMessage]: def django_settings(app_configs, **kwargs) -> List[CheckMessage]:
errors: list[CheckMessage] = [] errors: List[CheckMessage] = []
if hasattr(settings, "SITE_URL"): if hasattr(settings, "SITE_URL"):
if settings.SITE_URL[-1] == "/": 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")) errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005"))
@@ -32,15 +33,15 @@ def django_settings(app_configs, **kwargs) -> list[CheckMessage]:
@register() @register()
def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]: def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]:
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=datetime.timezone.utc): if 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(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001")) errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001"))
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://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A002")) errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A002"))
@@ -53,8 +54,8 @@ def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]:
@register() @register()
def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]: def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]:
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":
@@ -65,7 +66,7 @@ def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]:
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=datetime.timezone.utc): if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=timezone.utc):
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004")) errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004"))
elif mysql_version.major == 8 and mysql_version.minor == 3: elif mysql_version.major == 8 and mysql_version.minor == 3:
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005")) errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005"))
@@ -73,7 +74,7 @@ def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]:
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")) errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A006"))
elif mysql_version.major == 8 and mysql_version.minor == 1: elif mysql_version.major == 8 and mysql_version.minor == 1:
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007")) errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007"))
elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=datetime.timezone.utc): elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=timezone.utc):
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008")) 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 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")) 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"))
@@ -81,8 +82,8 @@ def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]:
@register() @register()
def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]:
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 if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB
@@ -93,25 +94,25 @@ def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]:
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=datetime.timezone.utc): if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010"))
elif mariadb_version.major == 11 and mariadb_version.minor == 2: elif mariadb_version.major == 11 and mariadb_version.minor == 2:
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018")) errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018"))
if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=datetime.timezone.utc): if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011"))
elif mariadb_version.major == 11 and mariadb_version.minor == 1: elif mariadb_version.major == 11 and mariadb_version.minor == 1:
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019")) errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019"))
if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=datetime.timezone.utc): if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012")) 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 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")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", 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=datetime.timezone.utc): elif mariadb_version.major == 10 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(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.A014")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014"))
elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=datetime.timezone.utc): elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015"))
elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=datetime.timezone.utc): 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(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016"))
elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017")) errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017"))
@@ -120,8 +121,8 @@ def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]:
@register() @register()
def system_package_sqlite(app_configs, **kwargs) -> list[CheckMessage]: def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]:
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:
@@ -135,8 +136,8 @@ def system_package_sqlite(app_configs, **kwargs) -> list[CheckMessage]:
@register() @register()
def sql_settings(app_configs, **kwargs) -> list[CheckMessage]: def sql_settings(app_configs, **kwargs) -> List[CheckMessage]:
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:
@@ -158,8 +159,8 @@ 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] = [] errors: List[CheckMessage] = []
try: try:
if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}: if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}:

View File

@@ -0,0 +1,3 @@
"""
Initializes the custom_css module.
"""

View 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'

View 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")

View 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",
}
)
}

View 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": (),
},
),
]

View 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

View File

@@ -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 %}

View File

@@ -0,0 +1,3 @@
{% load custom_css %}
{% custom_css_static 'allianceauth/custom-styles.css' %}

View File

@@ -0,0 +1,3 @@
"""
Init file for custom_css templatetags
"""

View 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}">')

View 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",
# )

View File

@@ -235,7 +235,7 @@ class EveCharacter(models.Model):
return self.corporation_id == DOOMHEIM_CORPORATION_ID return self.corporation_id == DOOMHEIM_CORPORATION_ID
@property @property
def alliance(self) -> EveAllianceInfo | None: def alliance(self) -> Union[EveAllianceInfo, None]:
""" """
Pseudo foreign key from alliance_id to EveAllianceInfo Pseudo foreign key from alliance_id to EveAllianceInfo
:raises: EveAllianceInfo.DoesNotExist :raises: EveAllianceInfo.DoesNotExist
@@ -255,7 +255,7 @@ class EveCharacter(models.Model):
return EveCorporationInfo.objects.get(corporation_id=self.corporation_id) return EveCorporationInfo.objects.get(corporation_id=self.corporation_id)
@property @property
def faction(self) -> EveFactionInfo | None: def faction(self) -> Union[EveFactionInfo, None]:
""" """
Pseudo foreign key from faction_id to EveFactionInfo Pseudo foreign key from faction_id to EveFactionInfo
:raises: EveFactionInfo.DoesNotExist :raises: EveFactionInfo.DoesNotExist

View File

@@ -13,7 +13,7 @@ from allianceauth.framework.api.user import get_sentinel_user
def get_main_character_from_evecharacter( def get_main_character_from_evecharacter(
character: EveCharacter, character: EveCharacter,
) -> EveCharacter | None: ) -> Optional[EveCharacter]:
""" """
Get the main character for a given EveCharacter or None when no main character is set Get the main character for a given EveCharacter or None when no main character is set

View File

@@ -34,7 +34,7 @@ def get_all_characters_from_user(user: User) -> list:
return characters return characters
def get_main_character_from_user(user: User) -> EveCharacter | None: def get_main_character_from_user(user: User) -> Optional[EveCharacter]:
""" """
Get the main character from a user Get the main character from a user

View File

@@ -181,7 +181,7 @@ class AuthGroup(models.Model):
def __str__(self): def __str__(self):
return self.group.name return self.group.name
def group_request_approvers(self) -> set[User]: def group_request_approvers(self) -> Set[User]:
"""Return all users who can approve a group request.""" """Return all users who can approve a group request."""
return set( return set(
self.group_leaders.all() self.group_leaders.all()

View File

@@ -5,7 +5,7 @@ from typing import Optional
class ApplicationManager(models.Manager): class ApplicationManager(models.Manager):
def pending_requests_count_for_user(self, user: User) -> int | None: def pending_requests_count_for_user(self, user: User) -> Optional[int]:
"""Returns the number of pending group requests for the given user""" """Returns the number of pending group requests for the given user"""
if user.is_superuser: if user.is_superuser:
return self.filter(approved__isnull=True).count() return self.filter(approved__isnull=True).count()

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-12 19:15+1000\n" "POT-Creation-Date: 2024-09-09 13:05+1000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -88,27 +88,31 @@ msgstr ""
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:96 #: allianceauth/authentication/models.py:81
msgid "Polish"
msgstr ""
#: allianceauth/authentication/models.py:97
#: allianceauth/menu/templates/menu/menu-user.html:42 #: allianceauth/menu/templates/menu/menu-user.html:42
msgid "Language" msgid "Language"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:101 #: allianceauth/authentication/models.py:102
#: allianceauth/templates/allianceauth/night-toggle.html:6 #: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode" msgid "Night Mode"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:105 #: allianceauth/authentication/models.py:106
#: allianceauth/menu/templates/menu/menu-user.html:46 #: allianceauth/menu/templates/menu/menu-user.html:46
msgid "Theme" msgid "Theme"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:122 #: allianceauth/authentication/models.py:123
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:123 #: allianceauth/authentication/models.py:124
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "" msgstr ""
@@ -120,27 +124,27 @@ msgstr ""
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:7 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:5
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:33 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:33
#: allianceauth/hrapplications/templates/hrapplications/view.html:54 #: allianceauth/hrapplications/templates/hrapplications/view.html:54
msgid "Characters" msgid "Characters"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:13 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:11
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:14 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:12
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:4 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:4
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:6 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:6
msgid "Add Character" msgid "Add Character"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:16 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:14
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:17 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:15
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:10 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:10
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:12 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:12
msgid "Change Main" msgid "Change Main"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:24 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:22
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:89 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:89
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:31 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:31
@@ -149,12 +153,12 @@ msgstr ""
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:25 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:23
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:33 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:33
msgid "Corp" msgid "Corp"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_characters.html:26 #: allianceauth/authentication/templates/authentication/dashboard_characters.html:24
#: allianceauth/corputils/templates/corputils/corpstats.html:125 #: allianceauth/corputils/templates/corputils/corpstats.html:125
#: allianceauth/hrapplications/templates/hrapplications/view.html:63 #: allianceauth/hrapplications/templates/hrapplications/view.html:63
msgid "Alliance" msgid "Alliance"
@@ -164,7 +168,7 @@ msgstr ""
msgid "Membership" msgid "Membership"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard_groups.html:8 #: allianceauth/authentication/templates/authentication/dashboard_groups.html:10
msgid "State:" msgid "State:"
msgstr "" msgstr ""
@@ -395,6 +399,19 @@ msgstr ""
msgid "Failed to gather corporation statistics with selected token." msgid "Failed to gather corporation statistics with selected token."
msgstr "" msgstr ""
#: allianceauth/custom_css/apps.py:13 allianceauth/custom_css/models.py:36
#: allianceauth/custom_css/models.py:37 allianceauth/custom_css/models.py:47
msgid "Custom CSS"
msgstr ""
#: allianceauth/custom_css/models.py:25
msgid "Your custom CSS"
msgstr ""
#: allianceauth/custom_css/models.py:26
msgid "This CSS will be added to the site after the default CSS."
msgstr ""
#: allianceauth/fleetactivitytracking/auth_hooks.py:10 #: allianceauth/fleetactivitytracking/auth_hooks.py:10
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:10 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:10
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkcreate.html:11 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkcreate.html:11
@@ -487,8 +504,8 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:37 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:37
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:42 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:42
#: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:37
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:16 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:17
#: allianceauth/timerboard/templates/timerboard/timertable.html:9 #: allianceauth/timerboard/templates/timerboard/timertable.html:9
msgid "System" msgid "System"
msgstr "" msgstr ""
@@ -807,7 +824,7 @@ msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31
#: allianceauth/optimer/templates/optimer/dashboard.ops.html:15 #: allianceauth/optimer/templates/optimer/dashboard.ops.html:15
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:15 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:16
msgid "Type" msgid "Type"
msgstr "" msgstr ""
@@ -899,7 +916,7 @@ msgid "Hidden"
msgstr "" msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:45 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:45
#: allianceauth/templates/allianceauth/admin-status/overview.html:19 #: allianceauth/templates/allianceauth/admin-status/overview.html:16
msgid "Open" msgid "Open"
msgstr "" msgstr ""
@@ -1375,16 +1392,16 @@ msgstr ""
msgid "Super User" msgid "Super User"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:68 #: allianceauth/menu/templates/menu/menu-user.html:70
#: allianceauth/templates/allianceauth/top-menu-admin.html:9 #: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin" msgid "Admin"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:80 #: allianceauth/menu/templates/menu/menu-user.html:82
msgid "Sign Out" msgid "Sign Out"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:84 #: allianceauth/menu/templates/menu/menu-user.html:86
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:17 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:17
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:18 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:18
msgid "Sign In" msgid "Sign In"
@@ -1534,7 +1551,7 @@ msgid "Form Up System"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/dashboard.ops.html:17 #: allianceauth/optimer/templates/optimer/dashboard.ops.html:17
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:17 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:18
msgid "EVE Time" msgid "EVE Time"
msgstr "" msgstr ""
@@ -1729,17 +1746,17 @@ msgid ""
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:26 #: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:26
#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:20 #: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:22
msgid "Activate" msgid "Activate"
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:32 #: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:32
#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:32 #: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:34
msgid "Reset Password" msgid "Reset Password"
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:38 #: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:38
#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:38 #: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:40
msgid "Deactivate" msgid "Deactivate"
msgstr "" msgstr ""
@@ -1819,12 +1836,12 @@ msgstr ""
msgid "Deactivated IPSuite4 account." msgid "Deactivated IPSuite4 account."
msgstr "" msgstr ""
#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:26 #: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:28
#: allianceauth/services/templates/services/service_password.html:26 #: allianceauth/services/templates/services/service_password.html:26
msgid "Set Password" msgid "Set Password"
msgstr "" msgstr ""
#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:44 #: allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html:46
msgid "Connect" msgid "Connect"
msgstr "" msgstr ""
@@ -2386,56 +2403,56 @@ msgstr ""
msgid "Your Server received an ESI error response code of " msgid "Your Server received an ESI error response code of "
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:11 #: allianceauth/templates/allianceauth/admin-status/overview.html:8
msgid "Alliance Auth Notifications" msgid "Alliance Auth Notifications"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:21 #: allianceauth/templates/allianceauth/admin-status/overview.html:18
msgid "Closed" msgid "Closed"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:27 #: allianceauth/templates/allianceauth/admin-status/overview.html:24
msgid "No notifications at this time" msgid "No notifications at this time"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:36 #: allianceauth/templates/allianceauth/admin-status/overview.html:33
msgid "Powered by GitLab" msgid "Powered by GitLab"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:42 #: allianceauth/templates/allianceauth/admin-status/overview.html:39
msgid "Support Discord" msgid "Support Discord"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:59 #: allianceauth/templates/allianceauth/admin-status/overview.html:53
#: allianceauth/templates/allianceauth/admin-status/overview.html:63 #: allianceauth/templates/allianceauth/admin-status/overview.html:57
msgid "Software Version" msgid "Software Version"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:66 #: allianceauth/templates/allianceauth/admin-status/overview.html:60
msgid "Current" msgid "Current"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:73 #: allianceauth/templates/allianceauth/admin-status/overview.html:67
msgid "Latest Stable" msgid "Latest Stable"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:78 #: allianceauth/templates/allianceauth/admin-status/overview.html:72
msgid "Update available" msgid "Update available"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:86 #: allianceauth/templates/allianceauth/admin-status/overview.html:80
msgid "Latest Pre-Release" msgid "Latest Pre-Release"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:91 #: allianceauth/templates/allianceauth/admin-status/overview.html:85
msgid "Pre-Release available" msgid "Pre-Release available"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:102 #: allianceauth/templates/allianceauth/admin-status/overview.html:95
msgid "Task Queue" msgid "Task Queue"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:107 #: allianceauth/templates/allianceauth/admin-status/overview.html:100
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@@ -2444,11 +2461,11 @@ msgid ""
" " " "
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:123 #: allianceauth/templates/allianceauth/admin-status/overview.html:116
msgid "running" msgid "running"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:124 #: allianceauth/templates/allianceauth/admin-status/overview.html:117
msgid "queued" msgid "queued"
msgstr "" msgstr ""
@@ -2477,105 +2494,203 @@ msgstr ""
msgid "Select Theme" msgid "Select Theme"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:53 #: allianceauth/timerboard/form.py:36
#: allianceauth/timerboard/templates/timerboard/timertable.html:172 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:14
msgid "Other"
msgstr ""
#: allianceauth/timerboard/form.py:54
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:38
#: allianceauth/timerboard/templates/timerboard/timertable.html:44
msgid "Friendly"
msgstr ""
#: allianceauth/timerboard/form.py:55
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:33
#: allianceauth/timerboard/templates/timerboard/timertable.html:38
msgid "Hostile"
msgstr ""
#: allianceauth/timerboard/form.py:56
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:43
#: allianceauth/timerboard/templates/timerboard/timertable.html:50
msgid "Neutral"
msgstr ""
#: allianceauth/timerboard/form.py:58
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:13
#: allianceauth/timerboard/templates/timerboard/timertable.html:7 #: allianceauth/timerboard/templates/timerboard/timertable.html:7
msgid "Details" msgid "Details"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:60 #: allianceauth/timerboard/form.py:38
msgid "Planet/Moon" msgid "Planet/Moon"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:61 #: allianceauth/timerboard/form.py:39
msgid "Structure Type" msgid "Structure Type"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:62 #: allianceauth/timerboard/form.py:40
msgid "Timer Type" msgid "Timer Type"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:63 #: allianceauth/timerboard/form.py:41
#: allianceauth/timerboard/templates/timerboard/timertable.html:8 #: allianceauth/timerboard/templates/timerboard/timertable.html:8
msgid "Objective" msgid "Objective"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:64 #: allianceauth/timerboard/form.py:42
msgid "Absolute Timer" msgid "Absolute Timer"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:65 #: allianceauth/timerboard/form.py:43
msgid "Date and Time" msgid "Date and Time"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:66 #: allianceauth/timerboard/form.py:44
msgid "Days Remaining" msgid "Days Remaining"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:67 #: allianceauth/timerboard/form.py:45
msgid "Hours Remaining" msgid "Hours Remaining"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:69 #: allianceauth/timerboard/form.py:47
msgid "Minutes Remaining" msgid "Minutes Remaining"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:71 #: allianceauth/timerboard/form.py:48
msgid "Important" msgid "Important"
msgstr "" msgstr ""
#: allianceauth/timerboard/form.py:72 #: allianceauth/timerboard/form.py:49
msgid "Corp-Restricted" msgid "Corp-Restricted"
msgstr "" msgstr ""
#: allianceauth/timerboard/models.py:13
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:39
#: allianceauth/timerboard/templates/timerboard/timertable.html:36
msgid "Friendly"
msgstr ""
#: allianceauth/timerboard/models.py:14 #: allianceauth/timerboard/models.py:14
msgid "Not Specified" #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:34
#: allianceauth/timerboard/templates/timerboard/timertable.html:34
msgid "Hostile"
msgstr "" msgstr ""
#: allianceauth/timerboard/models.py:15 #: allianceauth/timerboard/models.py:15
msgid "Shield" #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:44
msgstr "" #: allianceauth/timerboard/templates/timerboard/timertable.html:38
msgid "Neutral"
#: allianceauth/timerboard/models.py:16
msgid "Armor"
msgstr ""
#: allianceauth/timerboard/models.py:17
msgid "Hull"
msgstr "" msgstr ""
#: allianceauth/timerboard/models.py:18 #: allianceauth/timerboard/models.py:18
msgid "Final" #: allianceauth/timerboard/templates/timerboard/timertable.html:48
msgid "POCO"
msgstr "" msgstr ""
#: allianceauth/timerboard/models.py:19 #: allianceauth/timerboard/models.py:19
msgid "Anchoring" #: allianceauth/timerboard/templates/timerboard/timertable.html:50
msgid "Orbital Skyhook"
msgstr "" msgstr ""
#: allianceauth/timerboard/models.py:20 #: allianceauth/timerboard/models.py:20
#: allianceauth/timerboard/templates/timerboard/timertable.html:52
msgid "I-HUB"
msgstr ""
#: allianceauth/timerboard/models.py:21
#: allianceauth/timerboard/templates/timerboard/timertable.html:54
msgid "TCU"
msgstr ""
#: allianceauth/timerboard/models.py:22
#: allianceauth/timerboard/templates/timerboard/timertable.html:56
msgid "POS [S]"
msgstr ""
#: allianceauth/timerboard/models.py:23
#: allianceauth/timerboard/templates/timerboard/timertable.html:58
msgid "POS [M]"
msgstr ""
#: allianceauth/timerboard/models.py:24
#: allianceauth/timerboard/templates/timerboard/timertable.html:60
msgid "POS [L]"
msgstr ""
#: allianceauth/timerboard/models.py:25
#: allianceauth/timerboard/templates/timerboard/timertable.html:62
msgid "Astrahus"
msgstr ""
#: allianceauth/timerboard/models.py:26
#: allianceauth/timerboard/templates/timerboard/timertable.html:64
msgid "Fortizar"
msgstr ""
#: allianceauth/timerboard/models.py:27
#: allianceauth/timerboard/templates/timerboard/timertable.html:66
msgid "Keepstar"
msgstr ""
#: allianceauth/timerboard/models.py:28
#: allianceauth/timerboard/templates/timerboard/timertable.html:68
msgid "Raitaru"
msgstr ""
#: allianceauth/timerboard/models.py:29
#: allianceauth/timerboard/templates/timerboard/timertable.html:70
msgid "Azbel"
msgstr ""
#: allianceauth/timerboard/models.py:30
#: allianceauth/timerboard/templates/timerboard/timertable.html:72
msgid "Sotiyo"
msgstr ""
#: allianceauth/timerboard/models.py:31
#: allianceauth/timerboard/templates/timerboard/timertable.html:74
msgid "Athanor"
msgstr ""
#: allianceauth/timerboard/models.py:32
#: allianceauth/timerboard/templates/timerboard/timertable.html:76
msgid "Tatara"
msgstr ""
#: allianceauth/timerboard/models.py:33
msgid "Pharolux Cyno Beacon"
msgstr ""
#: allianceauth/timerboard/models.py:34
msgid "Tenebrex Cyno Jammer"
msgstr ""
#: allianceauth/timerboard/models.py:35
#: allianceauth/timerboard/templates/timerboard/timertable.html:82
msgid "Ansiblex Jump Gate"
msgstr ""
#: allianceauth/timerboard/models.py:36
#: allianceauth/timerboard/templates/timerboard/timertable.html:84
msgid "Moon Mining Cycle"
msgstr ""
#: allianceauth/timerboard/models.py:37
#: allianceauth/timerboard/templates/timerboard/timertable.html:86
msgid "Metenox Moon Drill"
msgstr ""
#: allianceauth/timerboard/models.py:38
#: allianceauth/timerboard/templates/timerboard/timertable.html:88
msgid "Other"
msgstr ""
#: allianceauth/timerboard/models.py:45
msgid "Not Specified"
msgstr ""
#: allianceauth/timerboard/models.py:46
msgid "Shield"
msgstr ""
#: allianceauth/timerboard/models.py:47
msgid "Armor"
msgstr ""
#: allianceauth/timerboard/models.py:48
msgid "Hull"
msgstr ""
#: allianceauth/timerboard/models.py:49
msgid "Final"
msgstr ""
#: allianceauth/timerboard/models.py:50
msgid "Anchoring"
msgstr ""
#: allianceauth/timerboard/models.py:51
msgid "Unanchoring" msgid "Unanchoring"
msgstr "" msgstr ""
@@ -2584,7 +2699,7 @@ msgstr ""
msgid "Upcoming Timers" msgid "Upcoming Timers"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:14 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:15
msgid "Timer" msgid "Timer"
msgstr "" msgstr ""
@@ -2628,78 +2743,14 @@ msgstr ""
msgid "Structure" msgid "Structure"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:64 #: allianceauth/timerboard/templates/timerboard/timertable.html:78
msgid "POCO"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:70
msgid "I-HUB"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:76
msgid "TCU"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:82
msgid "POS [S]"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:88
msgid "POS [M]"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:94
msgid "POS [L]"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:100
msgid "Astrahus"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:106
msgid "Fortizar"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:112
msgid "Keepstar"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:118
msgid "Raitaru"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:124
msgid "Azbel"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:130
msgid "Sotiyo"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:136
msgid "Athanor"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:142
msgid "Tatara"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:148
msgid "Cyno Beacon" msgid "Cyno Beacon"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:154 #: allianceauth/timerboard/templates/timerboard/timertable.html:80
msgid "Cyno Jammer" msgid "Cyno Jammer"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:160
msgid "Ansiblex Jump Gate"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/timertable.html:166
msgid "Moon Mining Cycle"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:9 #: allianceauth/timerboard/templates/timerboard/view.html:9
msgid "Structure Timer Management" msgid "Structure Timer Management"
msgstr "" msgstr ""

View File

@@ -35,12 +35,12 @@ class MenuItemAdmin(admin.ModelAdmin):
] ]
ordering = ["parent", "order", "text"] ordering = ["parent", "order", "text"]
def get_form(self, request: HttpRequest, obj: MenuItem | None = None, **kwargs): def get_form(self, request: HttpRequest, obj: Optional[MenuItem] = None, **kwargs):
kwargs["form"] = self._choose_form(request, obj) kwargs["form"] = self._choose_form(request, obj)
return super().get_form(request, obj, **kwargs) return super().get_form(request, obj, **kwargs)
@classmethod @classmethod
def _choose_form(cls, request: HttpRequest, obj: MenuItem | None): def _choose_form(cls, request: HttpRequest, obj: Optional[MenuItem]):
"""Return the form for the current menu item type.""" """Return the form for the current menu item type."""
if obj: # change if obj: # change
if obj.hook_hash: if obj.hook_hash:
@@ -104,7 +104,7 @@ class MenuItemAdmin(admin.ModelAdmin):
@staticmethod @staticmethod
def _type_from_request( def _type_from_request(
request: HttpRequest, default=None request: HttpRequest, default=None
) -> MenuItemType | None: ) -> Optional[MenuItemType]:
try: try:
return MenuItemType(request.GET.get("type")) return MenuItemType(request.GET.get("type"))
except ValueError: except ValueError:

View File

@@ -14,8 +14,8 @@ class MenuItemHookCustom(MenuItemHook):
text: str, text: str,
classes: str, classes: str,
url_name: str, url_name: str,
order: int | None = None, order: Optional[int] = None,
navactive: list[str] | None = None, navactive: Optional[List[str]] = None,
): ):
super().__init__(text, classes, url_name, order, navactive) super().__init__(text, classes, url_name, order, navactive)
self.url = "" self.url = ""

View File

@@ -33,8 +33,8 @@ class MenuItemHook:
text: str, text: str,
classes: str, classes: str,
url_name: str, url_name: str,
order: int | None = None, order: Optional[int] = None,
navactive: list[str] | None = None, navactive: Optional[List[str]] = None,
): ):
self.text = text self.text = text
self.classes = classes self.classes = classes

View File

@@ -56,8 +56,8 @@ class RenderedMenuItem:
menu_item: MenuItem menu_item: MenuItem
children: list["RenderedMenuItem"] = field(default_factory=list) children: List["RenderedMenuItem"] = field(default_factory=list)
count: int | None = None count: Optional[int] = None
html: str = "" html: str = ""
html_id: str = "" html_id: str = ""
@@ -78,7 +78,7 @@ class RenderedMenuItem:
self.html_id = hook_obj.html_id self.html_id = hook_obj.html_id
def render_menu(request: HttpRequest) -> list[RenderedMenuItem]: def render_menu(request: HttpRequest) -> List[RenderedMenuItem]:
"""Return the rendered side menu for including in a template. """Return the rendered side menu for including in a template.
This function is creating BS5 style menus. This function is creating BS5 style menus.
@@ -88,7 +88,7 @@ def render_menu(request: HttpRequest) -> list[RenderedMenuItem]:
# Menu items needs to be rendered with the new BS5 template # Menu items needs to be rendered with the new BS5 template
bs5_template = "menu/menu-item-bs5.html" bs5_template = "menu/menu-item-bs5.html"
rendered_items: dict[int, RenderedMenuItem] = {} rendered_items: Dict[int, RenderedMenuItem] = {}
menu_items: QuerySet[MenuItem] = MenuItem.objects.order_by( menu_items: QuerySet[MenuItem] = MenuItem.objects.order_by(
"parent", "order", "text" "parent", "order", "text"
) )
@@ -131,7 +131,7 @@ def render_menu(request: HttpRequest) -> list[RenderedMenuItem]:
return list(rendered_items.values()) return list(rendered_items.values())
def _gather_menu_items_from_hooks() -> dict[str, MenuItemHook]: def _gather_menu_items_from_hooks() -> Dict[str, MenuItemHook]:
hook_items = {} hook_items = {}
for hook in get_hooks("menu_item_hook"): for hook in get_hooks("menu_item_hook"):
f = hook() f = hook()
@@ -161,14 +161,14 @@ def _render_link_item(
def _render_folder_items( def _render_folder_items(
request: HttpRequest, rendered_items: dict[int, RenderedMenuItem], new_template: str request: HttpRequest, rendered_items: Dict[int, RenderedMenuItem], new_template: str
): ):
for item in rendered_items.values(): for item in rendered_items.values():
if item.menu_item.is_folder: if item.menu_item.is_folder:
item.update_html(request=request, template=new_template) item.update_html(request=request, template=new_template)
def _remove_empty_folders(rendered_items: dict[int, RenderedMenuItem]): def _remove_empty_folders(rendered_items: Dict[int, RenderedMenuItem]):
ids_to_remove = [] ids_to_remove = []
for item_id, item in rendered_items.items(): for item_id, item in rendered_items.items():
if item.is_folder and not item.children: if item.is_folder and not item.children:

View File

@@ -347,9 +347,9 @@ class TestRenderedMenuItem(TestCase):
class _ParsedMenuItem(NamedTuple): class _ParsedMenuItem(NamedTuple):
classes: list[str] classes: List[str]
text: str text: str
count: int | None count: Optional[int]
def parse_html(obj: RenderedMenuItem) -> _ParsedMenuItem: def parse_html(obj: RenderedMenuItem) -> _ParsedMenuItem:

View File

@@ -22,6 +22,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.humanize', 'django.contrib.humanize',
'django_celery_beat', 'django_celery_beat',
'solo',
'bootstrapform', 'bootstrapform',
'django_bootstrap5', # https://github.com/zostera/django-bootstrap5 'django_bootstrap5', # https://github.com/zostera/django-bootstrap5
'sortedm2m', 'sortedm2m',
@@ -39,6 +40,7 @@ INSTALLED_APPS = [
'allianceauth.theme.darkly', 'allianceauth.theme.darkly',
'allianceauth.theme.flatly', 'allianceauth.theme.flatly',
'allianceauth.theme.materia', 'allianceauth.theme.materia',
"allianceauth.custom_css",
] ]
SECRET_KEY = "wow I'm a really bad default secret key" SECRET_KEY = "wow I'm a really bad default secret key"

View File

@@ -1,7 +1,6 @@
from string import Formatter from string import Formatter
from django.urls import include, re_path from django.urls import include, re_path
from typing import Optional from typing import Iterable, Optional
from collections.abc import Iterable
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -176,7 +175,7 @@ class UrlHook:
urls, urls,
namespace: str, namespace: str,
base_url: str, base_url: str,
excluded_views : Iterable[str] | None = None excluded_views : Optional[Iterable[str]] = None
): ):
self.include_pattern = re_path(base_url, include(urls, namespace=namespace)) self.include_pattern = re_path(base_url, include(urls, namespace=namespace))
self.excluded_views = set(excluded_views or []) self.excluded_views = set(excluded_views or [])

View File

@@ -28,7 +28,7 @@ from .models import DiscordUser # noqa
__all__ = ["create_bot_client", "group_to_role", "server_name", "DiscordUser", "Role"] __all__ = ["create_bot_client", "group_to_role", "server_name", "DiscordUser", "Role"]
def discord_guild_id() -> int | None: def discord_guild_id() -> Optional[int]:
"""Guild ID of configured Discord server. """Guild ID of configured Discord server.
Returns: Returns:

View File

@@ -36,7 +36,7 @@ def calculate_roles_for_user(
client: DiscordClient, client: DiscordClient,
discord_uid: int, discord_uid: int,
state_name: str = None, state_name: str = None,
) -> tuple[RolesSet, bool | None]: ) -> Tuple[RolesSet, Optional[bool]]:
"""Calculate current Discord roles for an Auth user. """Calculate current Discord roles for an Auth user.
Takes into account reserved groups and existing managed roles (e.g. nitro). Takes into account reserved groups and existing managed roles (e.g. nitro).
@@ -68,7 +68,7 @@ def calculate_roles_for_user(
return roles_calculated.union(roles_persistent), True return roles_calculated.union(roles_persistent), True
def _user_group_names(user: User, state_name: str = None) -> list[str]: def _user_group_names(user: User, state_name: str = None) -> List[str]:
"""Names of groups and state the given user is a member of.""" """Names of groups and state the given user is a member of."""
if not state_name: if not state_name:
state_name = user.profile.state.name state_name = user.profile.state.name
@@ -77,7 +77,7 @@ def _user_group_names(user: User, state_name: str = None) -> list[str]:
return group_names return group_names
def user_formatted_nick(user: User) -> str | None: def user_formatted_nick(user: User) -> Optional[str]:
"""Name of the given user's main character with name formatting applied. """Name of the given user's main character with name formatting applied.
Returns: Returns:
@@ -90,7 +90,7 @@ def user_formatted_nick(user: User) -> str | None:
return None return None
def group_to_role(group: Group) -> Role | None: def group_to_role(group: Group) -> Optional[Role]:
"""Fetch the Discord role matching the given Django group by name. """Fetch the Discord role matching the given Django group by name.
Returns: Returns:

View File

@@ -6,8 +6,7 @@ from enum import IntEnum
from hashlib import md5 from hashlib import md5
from http import HTTPStatus from http import HTTPStatus
from time import sleep from time import sleep
from typing import List, Optional, Set, Tuple from typing import Iterable, List, Optional, Set, Tuple
from collections.abc import Iterable
from urllib.parse import urljoin from urllib.parse import urljoin
from uuid import uuid1 from uuid import uuid1
@@ -234,7 +233,7 @@ class DiscordClient:
# guild roles # guild roles
def guild_roles(self, guild_id: int, use_cache: bool = True) -> set[Role]: def guild_roles(self, guild_id: int, use_cache: bool = True) -> Set[Role]:
"""Fetch all roles for this guild. """Fetch all roles for this guild.
Args: Args:
@@ -269,7 +268,7 @@ class DiscordClient:
def create_guild_role( def create_guild_role(
self, guild_id: int, role_name: str, **kwargs self, guild_id: int, role_name: str, **kwargs
) -> Role | None: ) -> Optional[Role]:
"""Create a new guild role with the given name. """Create a new guild role with the given name.
See official documentation for additional optional parameters. See official documentation for additional optional parameters.
@@ -319,7 +318,7 @@ class DiscordClient:
gen_key = cls._generate_hash(f'{guild_id}') gen_key = cls._generate_hash(f'{guild_id}')
return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}' return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}'
def match_role_from_name(self, guild_id: int, role_name: str) -> Role | None: def match_role_from_name(self, guild_id: int, role_name: str) -> Optional[Role]:
"""Fetch Discord role matching the given name (cached). """Fetch Discord role matching the given name (cached).
Args: Args:
@@ -334,7 +333,7 @@ class DiscordClient:
def match_or_create_roles_from_names( def match_or_create_roles_from_names(
self, guild_id: int, role_names: Iterable[str] self, guild_id: int, role_names: Iterable[str]
) -> list[tuple[Role, bool]]: ) -> List[Tuple[Role, bool]]:
"""Fetch or create Discord roles matching the given names (cached). """Fetch or create Discord roles matching the given names (cached).
Will try to match with existing roles names Will try to match with existing roles names
@@ -362,7 +361,7 @@ class DiscordClient:
def match_or_create_role_from_name( def match_or_create_role_from_name(
self, guild_id: int, role_name: str, guild_roles: RolesSet = None self, guild_id: int, role_name: str, guild_roles: RolesSet = None
) -> tuple[Role, bool]: ) -> Tuple[Role, bool]:
"""Fetch or create Discord role matching the given name. """Fetch or create Discord role matching the given name.
Will try to match with existing roles names Will try to match with existing roles names
@@ -419,7 +418,7 @@ class DiscordClient:
access_token: str, access_token: str,
role_ids: list = None, role_ids: list = None,
nick: str = None nick: str = None
) -> bool | None: ) -> Optional[bool]:
"""Adds a user to the guild. """Adds a user to the guild.
Returns: Returns:
@@ -443,7 +442,7 @@ class DiscordClient:
return None return None
return False return False
def guild_member(self, guild_id: int, user_id: int) -> GuildMember | None: def guild_member(self, guild_id: int, user_id: int) -> Optional[GuildMember]:
"""Fetch info for a guild member. """Fetch info for a guild member.
Args: Args:
@@ -462,8 +461,8 @@ class DiscordClient:
return GuildMember.from_dict(r.json()) return GuildMember.from_dict(r.json())
def modify_guild_member( def modify_guild_member(
self, guild_id: int, user_id: int, role_ids: list[int] = None, nick: str = None self, guild_id: int, user_id: int, role_ids: List[int] = None, nick: str = None
) -> bool | None: ) -> Optional[bool]:
"""Set properties of a guild member. """Set properties of a guild member.
Args: Args:
@@ -502,7 +501,7 @@ class DiscordClient:
return True return True
return False return False
def remove_guild_member(self, guild_id: int, user_id: int) -> bool | None: def remove_guild_member(self, guild_id: int, user_id: int) -> Optional[bool]:
"""Remove a member from a guild. """Remove a member from a guild.
Args: Args:
@@ -530,7 +529,7 @@ class DiscordClient:
def add_guild_member_role( def add_guild_member_role(
self, guild_id: int, user_id: int, role_id: int self, guild_id: int, user_id: int, role_id: int
) -> bool | None: ) -> Optional[bool]:
"""Adds a role to a guild member """Adds a role to a guild member
Returns: Returns:
@@ -550,7 +549,7 @@ class DiscordClient:
def remove_guild_member_role( def remove_guild_member_role(
self, guild_id: int, user_id: int, role_id: int self, guild_id: int, user_id: int, role_id: int
) -> bool | None: ) -> Optional[bool]:
"""Remove a role to a guild member """Remove a role to a guild member
Args: Args:
@@ -573,7 +572,7 @@ class DiscordClient:
return True return True
return False return False
def guild_member_roles(self, guild_id: int, user_id: int) -> RolesSet | None: def guild_member_roles(self, guild_id: int, user_id: int) -> Optional[RolesSet]:
"""Fetch the current guild roles of a guild member. """Fetch the current guild roles of a guild member.
Args: Args:
@@ -822,6 +821,6 @@ class DiscordClient:
return md5(key.encode('utf-8')).hexdigest() return md5(key.encode('utf-8')).hexdigest()
@staticmethod @staticmethod
def _sanitize_role_ids(role_ids: Iterable[int]) -> list[int]: def _sanitize_role_ids(role_ids: Iterable[int]) -> List[int]:
"""Sanitize a list of role IDs, i.e. make sure its a list of unique integers.""" """Sanitize a list of role IDs, i.e. make sure its a list of unique integers."""
return [int(role_id) for role_id in set(role_ids)] return [int(role_id) for role_id in set(role_ids)]

View File

@@ -1,6 +1,5 @@
from copy import copy from copy import copy
from typing import List, Optional, Set, Tuple from typing import Iterable, List, Optional, Set, Tuple
from collections.abc import Iterable
from .models import Role from .models import Role
@@ -51,7 +50,7 @@ class RolesSet:
def __len__(self): def __len__(self):
return len(self._roles.keys()) return len(self._roles.keys())
def has_roles(self, role_ids: set[int]) -> bool: def has_roles(self, role_ids: Set[int]) -> bool:
"""True if this objects contains all roles defined by given role_ids """True if this objects contains all roles defined by given role_ids
incl. managed roles. incl. managed roles.
""" """
@@ -59,7 +58,7 @@ class RolesSet:
all_role_ids = self._roles.keys() all_role_ids = self._roles.keys()
return role_ids.issubset(all_role_ids) return role_ids.issubset(all_role_ids)
def ids(self) -> set[int]: def ids(self) -> Set[int]:
"""Set of all role IDs.""" """Set of all role IDs."""
return set(self._roles.keys()) return set(self._roles.keys())
@@ -115,7 +114,7 @@ class RolesSet:
new_ids = self.ids().difference(other.ids()) new_ids = self.ids().difference(other.ids())
return self.subset(role_ids=new_ids) return self.subset(role_ids=new_ids)
def role_by_name(self, role_name: str) -> Role | None: def role_by_name(self, role_name: str) -> Optional[Role]:
"""Role if one with matching name is found else None.""" """Role if one with matching name is found else None."""
role_name = Role.sanitize_name(role_name) role_name = Role.sanitize_name(role_name)
if role_name in self._roles_by_name: if role_name in self._roles_by_name:
@@ -124,7 +123,7 @@ class RolesSet:
@classmethod @classmethod
def create_from_matched_roles( def create_from_matched_roles(
cls, matched_roles: list[tuple[Role, bool]] cls, matched_roles: List[Tuple[Role, bool]]
) -> "RolesSet": ) -> "RolesSet":
"""Create new instance from the given list of matches roles. """Create new instance from the given list of matches roles.

View File

@@ -69,7 +69,7 @@ class Guild:
id: int id: int
name: str name: str
roles: frozenset[Role] roles: FrozenSet[Role]
def __post_init__(self): def __post_init__(self):
object.__setattr__(self, "id", int(self.id)) object.__setattr__(self, "id", int(self.id))
@@ -95,7 +95,7 @@ class GuildMember:
_NICK_MAX_CHARS = 32 _NICK_MAX_CHARS = 32
roles: frozenset[int] roles: FrozenSet[int]
nick: str = None nick: str = None
user: User = None user: User = None

View File

@@ -98,7 +98,7 @@ class DiscordUser(models.Model):
logger.warning('Failed to update nickname for %s', self.user) logger.warning('Failed to update nickname for %s', self.user)
return success return success
def update_groups(self, state_name: str = None) -> bool | None: def update_groups(self, state_name: str = None) -> Optional[bool]:
"""update groups for a user based on his current group memberships. """update groups for a user based on his current group memberships.
Will add or remove roles of a user as needed. Will add or remove roles of a user as needed.
@@ -134,7 +134,7 @@ class DiscordUser(models.Model):
logger.info('No need to update roles for user %s', self.user) logger.info('No need to update roles for user %s', self.user)
return True return True
def update_username(self) -> bool | None: def update_username(self) -> Optional[bool]:
"""Updates the username incl. the discriminator """Updates the username incl. the discriminator
from the Discord server and saves it from the Discord server and saves it
@@ -159,7 +159,7 @@ class DiscordUser(models.Model):
notify_user: bool = False, notify_user: bool = False,
is_rate_limited: bool = True, is_rate_limited: bool = True,
handle_api_exceptions: bool = False handle_api_exceptions: bool = False
) -> bool | None: ) -> Optional[bool]:
"""Deletes the Discount user both on the server and locally """Deletes the Discount user both on the server and locally
Params: Params:

View File

@@ -438,7 +438,7 @@ class TestUserHasAccount(NoSocketsTestCase):
self.assertFalse(DiscordUser.objects.user_has_account(self.user)) self.assertFalse(DiscordUser.objects.user_has_account(self.user))
def test_return_false_if_user_does_not_exist(self): def test_return_false_if_user_does_not_exist(self):
my_user = AuthUtils.create_user("test_return_false_if_user_does_not_exist") my_user = User(username='Dummy')
self.assertFalse(DiscordUser.objects.user_has_account(my_user)) self.assertFalse(DiscordUser.objects.user_has_account(my_user))
def test_return_false_if_not_called_with_user_object(self): def test_return_false_if_not_called_with_user_object(self):

View File

@@ -181,7 +181,7 @@ class SmfManager:
return out return out
@classmethod @classmethod
def add_user(cls, username, email_address, groups, main_character: EveCharacter) -> tuple: def add_user(cls, username, email_address, groups, main_character: EveCharacter) -> Tuple:
""" """
Add a user to SMF Add a user to SMF
:param username: :param username:

View File

@@ -35,6 +35,8 @@
</style> </style>
{% block extra_css %}{% endblock extra_css %} {% block extra_css %}{% endblock extra_css %}
{% include 'custom_css/bundles/custom-css.html' %}
</head> </head>
<body> <body>

View File

@@ -262,7 +262,7 @@ class AuthUtils:
@classmethod @classmethod
def add_permissions_to_user_by_name( def add_permissions_to_user_by_name(
cls, perms: list[str], user: User, disconnect_signals: bool = True cls, perms: List[str], user: User, disconnect_signals: bool = True
) -> User: ) -> User:
"""Add permissions given by name to a user """Add permissions given by name to a user

View File

@@ -27,6 +27,7 @@ class BootstrapThemeHook(ThemeHook):
self, self,
"Bootstrap", "Bootstrap",
"Powerful, extensible, and feature-packed frontend toolkit.", "Powerful, extensible, and feature-packed frontend toolkit.",
html_tags={"data-theme": "bootstrap"},
css=CSS_STATICS, css=CSS_STATICS,
js=JS_STATICS, js=JS_STATICS,
header_padding="3.5em" header_padding="3.5em"
@@ -44,9 +45,9 @@ class BootstrapDarkThemeHook(ThemeHook):
self, self,
"Bootstrap Dark", "Bootstrap Dark",
"Powerful, extensible, and feature-packed frontend toolkit.", "Powerful, extensible, and feature-packed frontend toolkit.",
html_tags={"data-theme": "bootstrap-dark"},
css=CSS_STATICS, css=CSS_STATICS,
js=JS_STATICS, js=JS_STATICS,
html_tags="data-bs-theme=dark",
header_padding="3.5em" header_padding="3.5em"
) )

View File

@@ -13,6 +13,7 @@ class DarklyThemeHook(ThemeHook):
self, self,
"Darkly", "Darkly",
"Flatly in night mode!", "Flatly in night mode!",
html_tags={"data-theme": "darkly"},
css=[{ css=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/darkly/bootstrap.min.css", "url": "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/darkly/bootstrap.min.css",
"integrity": "sha512-HDszXqSUU0om4Yj5dZOUNmtwXGWDa5ppESlX98yzbBS+z+3HQ8a/7kcdI1dv+jKq+1V5b01eYurE7+yFjw6Rdg==" "integrity": "sha512-HDszXqSUU0om4Yj5dZOUNmtwXGWDa5ppESlX98yzbBS+z+3HQ8a/7kcdI1dv+jKq+1V5b01eYurE7+yFjw6Rdg=="

View File

@@ -13,6 +13,7 @@ class FlatlyThemeHook(ThemeHook):
self, self,
"Flatly", "Flatly",
"Flat and modern!", "Flat and modern!",
html_tags={"data-theme": "flatly"},
css=[{ css=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/flatly/bootstrap.min.css", "url": "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/flatly/bootstrap.min.css",
"integrity": "sha512-qoT4KwnRpAQ9uczPsw7GunsNmhRnYwSlE2KRCUPRQHSkDuLulCtDXuC2P/P6oqr3M5hoGagUG9pgHDPkD2zCDA==" "integrity": "sha512-qoT4KwnRpAQ9uczPsw7GunsNmhRnYwSlE2KRCUPRQHSkDuLulCtDXuC2P/P6oqr3M5hoGagUG9pgHDPkD2zCDA=="

View File

@@ -1,21 +1,21 @@
from typing import List, Optional from typing import List, Optional, Union
class ThemeHook: class ThemeHook:
""" """
Theme hook for injecting a Bootstrap 5 Theme and associated JS into alliance auth. Theme hook for injecting a Bootstrap 5 Theme and associated JS into alliance auth.
these can be local or CDN delivered These can be local or CDN delivered.
""" """
def __init__(self, def __init__(self,
name: str, name: str,
description: str, description: str,
css: list[dict], css: List[dict],
js: list[dict], js: List[dict],
css_template: str | None = None, css_template: Optional[str] = None,
js_template: str | None = None, js_template: Optional[str] = None,
html_tags: str | None = "", html_tags: Optional[Union[dict, str]] = None,
header_padding: str | None = "4em"): header_padding: Optional[str] = "4em"):
""" """
:param name: Theme python name :param name: Theme python name
:type name: str :type name: str
@@ -29,6 +29,10 @@ class ThemeHook:
:type css_template: Optional[str], optional :type css_template: Optional[str], optional
:param js_template: _description_, defaults to None :param js_template: _description_, defaults to None
:type js_template: Optional[str], optional :type js_template: Optional[str], optional
:param html_tags: Attributes added to the `<html>` tag, defaults to None
:type html_tags: Optional[dict|str], optional
:param header_padding: Top padding, defaults to "4em"
:type header_padding: Optional[str], optional
""" """
self.name = name self.name = name
self.description = description self.description = description
@@ -41,7 +45,11 @@ class ThemeHook:
self.css_template = css_template self.css_template = css_template
self.js_template = js_template self.js_template = js_template
self.html_tags = html_tags self.html_tags = (
" ".join([f"{key}={value}" for key, value in html_tags.items()])
if isinstance(html_tags, dict)
else html_tags
)
self.header_padding = header_padding self.header_padding = header_padding
def get_name(self): def get_name(self):
return f"{self.__class__.__module__}.{self.__class__.__name__}" return f"{self.__class__.__module__}.{self.__class__.__name__}"

View File

@@ -13,6 +13,7 @@ class MateriaThemeHook(ThemeHook):
self, self,
"Materia", "Materia",
"Material is the metaphor", "Material is the metaphor",
html_tags={"data-theme": "materia"},
css=[{ css=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/materia/bootstrap.min.css", "url": "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/materia/bootstrap.min.css",
"integrity": "sha512-2S9Do+uTmZmmJpdmAcOKdUrK/YslcvAuRfIF2ws8+BW9AvZXMRZM+o8Wq+PZrfISD6ZlIaeCWWZAdeprXIoYuQ==" "integrity": "sha512-2S9Do+uTmZmmJpdmAcOKdUrK/YslcvAuRfIF2ws8+BW9AvZXMRZM+o8Wq+PZrfISD6ZlIaeCWWZAdeprXIoYuQ=="

View File

@@ -1,11 +1,12 @@
import logging
import datetime import datetime
import logging
from django import forms from django import forms
from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Timer, TimerType from .models import Timer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -32,54 +33,28 @@ class TimerForm(forms.ModelForm):
kwargs.update({'initial': initial}) kwargs.update({'initial': initial})
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
structure_choices = [('POCO', 'POCO'),
('I-HUB', 'I-HUB'),
('TCU', 'TCU'),
('POS[S]', 'POS[S]'),
('POS[M]', 'POS[M]'),
('POS[L]', 'POS[L]'),
('Astrahus', 'Astrahus'),
('Fortizar', 'Fortizar'),
('Keepstar', 'Keepstar'),
('Raitaru', 'Raitaru'),
('Azbel', 'Azbel'),
('Sotiyo', 'Sotiyo'),
('Athanor', 'Athanor'),
('Tatara', 'Tatara'),
('Pharolux Cyno Beacon', 'Pharolux Cyno Beacon'),
('Tenebrex Cyno Jammer', 'Tenebrex Cyno Jammer'),
('Ansiblex Jump Gate', 'Ansiblex Jump Gate'),
('Moon Mining Cycle', 'Moon Mining Cycle'),
(_('Other'), _('Other'))]
objective_choices = [('Friendly', _('Friendly')),
('Hostile', _('Hostile')),
('Neutral', _('Neutral'))]
details = forms.CharField(max_length=254, required=True, label=_('Details')) details = forms.CharField(max_length=254, required=True, label=_('Details'))
system = forms.CharField(max_length=254, required=True, label=_("System")) system = forms.CharField(max_length=254, required=True, label=_("System"))
planet_moon = forms.CharField(max_length=254, label=_("Planet/Moon"), required=False, initial="") planet_moon = forms.CharField(max_length=254, label=_("Planet/Moon"), required=False, initial="")
structure = forms.ChoiceField(choices=structure_choices, required=True, label=_("Structure Type")) structure = forms.ChoiceField(choices=Timer.Structure.choices, required=True, label=_("Structure Type"))
timer_type = forms.ChoiceField(choices=TimerType.choices, label=_("Timer Type")) timer_type = forms.ChoiceField(choices=Timer.TimerType.choices, label=_("Timer Type"))
objective = forms.ChoiceField(choices=objective_choices, required=True, label=_("Objective")) objective = forms.ChoiceField(choices=Timer.Objective.choices, required=True, label=_("Objective"))
absolute_checkbox = forms.BooleanField(label=_("Absolute Timer"), required=False, initial=False) absolute_checkbox = forms.BooleanField(label=_("Absolute Timer"), required=False, initial=False)
absolute_time = forms.CharField(required=False,label=_("Date and Time")) absolute_time = forms.CharField(required=False,label=_("Date and Time"))
days_left = forms.IntegerField(required=False, label=_("Days Remaining"), validators=[MinValueValidator(0)]) days_left = forms.IntegerField(required=False, label=_("Days Remaining"), validators=[MinValueValidator(0)])
hours_left = forms.IntegerField(required=False, label=_("Hours Remaining"), hours_left = forms.IntegerField(required=False, label=_("Hours Remaining"),
validators=[MinValueValidator(0), MaxValueValidator(23)]) validators=[MinValueValidator(0), MaxValueValidator(23)])
minutes_left = forms.IntegerField(required=False, label=_("Minutes Remaining"), minutes_left = forms.IntegerField(required=False, label=_("Minutes Remaining"), validators=[MinValueValidator(0), MaxValueValidator(59)])
validators=[MinValueValidator(0), MaxValueValidator(59)])
important = forms.BooleanField(label=_("Important"), required=False) important = forms.BooleanField(label=_("Important"), required=False)
corp_timer = forms.BooleanField(label=_("Corp-Restricted"), required=False) corp_timer = forms.BooleanField(label=_("Corp-Restricted"), required=False)
def save(self, commit=True): def save(self, commit=True):
timer = super().save(commit=False) timer = super().save(commit=False)
# Get character # Get character
character = self.user.profile.main_character character = self.user.profile.main_character
corporation = character.corporation corporation = character.corporation
logger.debug("Determined timer save request on behalf " logger.debug(f"Determined timer save request on behalf of character {character} corporation {corporation}")
"of character {} corporation {}".format(character, corporation))
days_left = self.cleaned_data['days_left'] days_left = self.cleaned_data['days_left']
hours_left = self.cleaned_data['hours_left'] hours_left = self.cleaned_data['hours_left']

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2 on 2024-09-09 03:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('timerboard', '0005_alter_timer_planet_moon'),
]
operations = [
migrations.AlterField(
model_name='timer',
name='objective',
field=models.CharField(choices=[('Friendly', 'Friendly'), ('Hostile', 'Hostile'), ('Neutral', 'Neutral')], default='Neutral', max_length=254),
),
migrations.AlterField(
model_name='timer',
name='structure',
field=models.CharField(choices=[('POCO', 'POCO'), ('Orbital Skyhook', 'Orbital Skyhook'), ('I-HUB', 'I-HUB'), ('TCU', 'TCU'), ('POS[S]', 'POS [S]'), ('POS[M]', 'POS [M]'), ('POS[L]', 'POS [L]'), ('Astrahus', 'Astrahus'), ('Fortizar', 'Fortizar'), ('Keepstar', 'Keepstar'), ('Raitaru', 'Raitaru'), ('Azbel', 'Azbel'), ('Sotiyo', 'Sotiyo'), ('Athanor', 'Athanor'), ('Tatara', 'Tatara'), ('Pharolux Cyno Beacon', 'Pharolux Cyno Beacon'), ('Tenebrex Cyno Jammer', 'Tenebrex Cyno Jammer'), ('Ansiblex Jump Gate', 'Ansiblex Jump Gate'), ('Moon Mining Cycle', 'Moon Mining Cycle'), ('Metenox Moon Drill', 'Metenox Moon Drill'), ('Other', 'Other')], default='Other', max_length=254),
),
migrations.AlterField(
model_name='timer',
name='timer_type',
field=models.CharField(choices=[('UNSPECIFIED', 'Not Specified'), ('SHIELD', 'Shield'), ('ARMOR', 'Armor'), ('HULL', 'Hull'), ('FINAL', 'Final'), ('ANCHORING', 'Anchoring'), ('UNANCHORING', 'Unanchoring'), ('ABANDONED', 'Abandoned')], default='UNSPECIFIED', max_length=254),
),
]

View File

@@ -6,6 +6,43 @@ from allianceauth.eveonline.models import EveCharacter
from allianceauth.eveonline.models import EveCorporationInfo from allianceauth.eveonline.models import EveCorporationInfo
class Timer(models.Model):
class Objective(models.TextChoices):
"""
Choices for Objective Type
"""
FRIENDLY = "Friendly", _("Friendly")
HOSTILE = "Hostile", _("Hostile")
NEUTRAL = "Neutral", _("Neutral")
class Structure(models.TextChoices):
"""
Choices for Structure Type
"""
POCO = "POCO", _("POCO")
ORBITALSKYHOOK = "Orbital Skyhook", _("Orbital Skyhook")
IHUB = "I-HUB", _("I-HUB")
TCU = "TCU", _("TCU") # Pending Remval
POSS = "POS[S]", _("POS [S]")
POSM = "POS[M]", _("POS [M]")
POSL = "POS[L]", _("POS [L]")
ASTRAHUS = "Astrahus", _("Astrahus")
FORTIZAR = "Fortizar", _("Fortizar")
KEEPSTAR = "Keepstar", _("Keepstar")
RAITARU = "Raitaru", _("Raitaru")
AZBEL = "Azbel", _("Azbel")
SOTIYO = "Sotiyo", _("Sotiyo")
ATHANOR = "Athanor", _("Athanor")
TATARA = "Tatara", _("Tatara")
PHAROLUX = "Pharolux Cyno Beacon", _("Pharolux Cyno Beacon")
TENEBREX = "Tenebrex Cyno Jammer", _("Tenebrex Cyno Jammer")
ANSIBLEX = "Ansiblex Jump Gate", _("Ansiblex Jump Gate")
MOONPOP = "Moon Mining Cycle", _("Moon Mining Cycle")
METENOX = "Metenox Moon Drill", _("Metenox Moon Drill")
OTHER = "Other", _("Other")
class TimerType(models.TextChoices): class TimerType(models.TextChoices):
""" """
Choices for Timer Type Choices for Timer Type
@@ -18,22 +55,14 @@ class TimerType(models.TextChoices):
FINAL = "FINAL", _("Final") FINAL = "FINAL", _("Final")
ANCHORING = "ANCHORING", _("Anchoring") ANCHORING = "ANCHORING", _("Anchoring")
UNANCHORING = "UNANCHORING", _("Unanchoring") UNANCHORING = "UNANCHORING", _("Unanchoring")
ABANDONED = "ABANDONED", _("Abandoned")
class Timer(models.Model):
class Meta:
ordering = ['eve_time']
details = models.CharField(max_length=254, default="") details = models.CharField(max_length=254, default="")
system = models.CharField(max_length=254, default="") system = models.CharField(max_length=254, default="")
planet_moon = models.CharField(max_length=254, blank=True, default="") planet_moon = models.CharField(max_length=254, blank=True, default="")
structure = models.CharField(max_length=254, default="") structure = models.CharField(max_length=254,choices=Structure.choices,default=Structure.OTHER)
timer_type = models.CharField( timer_type = models.CharField(max_length=254,choices=TimerType.choices,default=TimerType.UNSPECIFIED)
max_length=254, objective = models.CharField(max_length=254, choices=Objective.choices, default=Objective.NEUTRAL)
choices=TimerType.choices,
default=TimerType.UNSPECIFIED,
)
objective = models.CharField(max_length=254, default="")
eve_time = models.DateTimeField() eve_time = models.DateTimeField()
important = models.BooleanField(default=False) important = models.BooleanField(default=False)
eve_character = models.ForeignKey(EveCharacter, null=True, on_delete=models.SET_NULL) eve_character = models.ForeignKey(EveCharacter, null=True, on_delete=models.SET_NULL)
@@ -41,5 +70,8 @@ class Timer(models.Model):
corp_timer = models.BooleanField(default=False) corp_timer = models.BooleanField(default=False)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
def __str__(self): def __str__(self) -> str:
return str(self.system) + ' ' + str(self.details) return str(self.system) + ' ' + str(self.details)
class Meta:
ordering = ['eve_time']

View File

@@ -40,7 +40,7 @@
</div> </div>
{% endif %} {% endif %}
{% if timer.objective == "Neutral" %} {% if timer.objective == "Neutral" %}
<div class="badge bg-default"> <div class="badge bg-secondary">
{% translate "Neutral" %} {% translate "Neutral" %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -18,174 +18,85 @@
</tr> </tr>
{% for timer in timers %} {% for timer in timers %}
{% if timer.important == True %} <tr class="{% if timer.important == True %}bg-danger bg-opacity-25{% else %}bg-info bg-opacity-25{% endif %}">
<tr class="bg-danger bg-opacity-25">
{% else %}
<tr class="bg-info bg-opacity-25">
{% endif %}
<td style="width: 150px;" class="text-center"> <td style="width: 150px;" class="text-center">
{{ timer.details }} {{ timer.details }}
{% if timer.timer_type != 'UNSPECIFIED' %} {% if timer.timer_type != 'UNSPECIFIED' %}
<br>({{ timer.get_timer_type_display }}) <br />
({{ timer.get_timer_type_display }})
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
{% if timer.objective == "Hostile" %} {% if timer.objective == "Hostile" %}
<div class="badge bg-danger"> <div class="badge bg-danger">{% translate "Hostile" %}</div>
{% translate "Hostile" %} {% elif timer.objective == "Friendly" %}
</div> <div class="badge bg-primary">{% translate "Friendly" %}</div>
{% endif %} {% elif timer.objective == "Neutral" %}
<div class="badge bg-secondary">{% translate "Neutral" %}</div>
{% if timer.objective == "Friendly" %}
<div class="badge bg-primary">
{% translate "Friendly" %}
</div>
{% endif %}
{% if timer.objective == "Neutral" %}
<div class="badge bg-default">
{% translate "Neutral" %}
</div>
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
<a href="{{ timer.system|dotlan_solar_system_url }}"> <a href="{{ timer.system|dotlan_solar_system_url }}">{{ timer.system }} {{ timer.planet_moon }}</a>
{{ timer.system }} {{ timer.planet_moon }}
</a>
</td> </td>
<td class="text-center"> <td class="text-center">
{% if timer.structure == "POCO" %} {% if timer.structure == "POCO" %}
<div class="badge bg-info"> <div class="badge bg-info">{% translate "POCO" %}</div>
{% translate "POCO" %} {% elif timer.structure == "Orbital Skyhook" %}
</div> <div class="badge bg-warning">{% translate "Orbital Skyhook" %}</div>
{% endif %} {% elif timer.structure == "I-HUB" %}
<div class="badge bg-warning">{% translate "I-HUB" %}</div>
{% if timer.structure == "I-HUB" %} {% elif timer.structure == "TCU" %} {% comment %} Pending Removal {% endcomment %}
<div class="badge bg-warning"> <div class="badge bg-secondary">{% translate "TCU" %}</div>
{% translate "I-HUB" %} {% elif timer.structure == "POS[S]" %}
</div> <div class="badge bg-info">{% translate "POS [S]" %}</div>
{% endif %} {% elif timer.structure == "POS[M]" %}
<div class="badge bg-info">{% translate "POS [M]" %}</div>
{% if timer.structure == "TCU" %} {% elif timer.structure == "POS[L]" %}
<div class="badge bg-danger"> <div class="badge bg-info">{% translate "POS [L]" %}</div>
{% translate "TCU" %} {% elif timer.structure == "Citadel[M]" or timer.structure == "Astrahus" %}
</div> <div class="badge bg-danger">{% translate "Astrahus" %}</div>
{% endif %} {% elif timer.structure == "Citadel[L]" or timer.structure == "Fortizar" %}
<div class="badge bg-danger">{% translate "Fortizar" %}</div>
{% if timer.structure == "POS[S]" %} {% elif timer.structure == "Citadel[XL]" or timer.structure == "Keepstar" %}
<div class="badge bg-info"> <div class="badge bg-danger">{% translate "Keepstar" %}</div>
{% translate "POS [S]" %} {% elif timer.structure == "Engineering Complex[M]" or timer.structure == "Raitaru" %}
</div> <div class="badge bg-warning">{% translate "Raitaru" %}</div>
{% endif %} {% elif timer.structure == "Engineering Complex[L]" or timer.structure == "Azbel" %}
<div class="badge bg-warning">{% translate "Azbel" %}</div>
{% if timer.structure == "POS[M]" %} {% elif timer.structure == "Engineering Complex[XL]" or timer.structure == "Sotiyo" %}
<div class="badge bg-info"> <div class="badge bg-danger">{% translate "Sotiyo" %}</div>
{% translate "POS [M]" %} {% elif timer.structure == "Refinery[M]" or timer.structure == "Athanor" %}
</div> <div class="badge bg-warning">{% translate "Athanor" %}</div>
{% endif %} {% elif timer.structure == "Refinery[L]" or timer.structure == "Tatara" %}
<div class="badge bg-warning">{% translate "Tatara" %}</div>
{% if timer.structure == "POS[L]" %} {% elif timer.structure == "Cyno Beacon" or timer.structure == "Pharolux Cyno Beacon" %}
<div class="badge bg-info"> <div class="badge bg-warning">{% translate "Cyno Beacon" %}</div>
{% translate "POS [L]" %} {% elif timer.structure == "Cyno Jammer" or timer.structure == "Tenebrex Cyno Jammer" %}
</div> <div class="badge bg-warning">{% translate "Cyno Jammer" %}</div>
{% endif %} {% elif timer.structure == "Jump Gate" or timer.structure == "Ansiblex Jump Gate" %}
<div class="badge bg-warning">{% translate "Ansiblex Jump Gate" %}</div>
{% if timer.structure == "Citadel[M]" or timer.structure == "Astrahus" %} {% elif timer.structure == "Moon Mining Cycle" %}
<div class="badge bg-danger"> <div class="badge bg-success">{% translate "Moon Mining Cycle" %}</div>
{% translate "Astrahus" %} {% elif timer.structure == "Metenox Moon Drill" %}
</div> <div class="badge bg-warning">{% translate "Metenox Moon Drill" %}</div>
{% endif %} {% elif timer.structure == "Other" %}
<div class="badge bg-secondary">{% translate "Other" %}</div>
{% if timer.structure == "Citadel[L]" or timer.structure == "Fortizar" %}
<div class="badge bg-danger">
{% translate "Fortizar" %}
</div>
{% endif %}
{% if timer.structure == "Citadel[XL]" or timer.structure == "Keepstar" %}
<div class="badge bg-danger">
{% translate "Keepstar" %}
</div>
{% endif %}
{% if timer.structure == "Engineering Complex[M]" or timer.structure == "Raitaru" %}
<div class="badge bg-warning">
{% translate "Raitaru" %}
</div>
{% endif %}
{% if timer.structure == "Engineering Complex[L]" or timer.structure == "Azbel" %}
<div class="badge bg-warning">
{% translate "Azbel" %}
</div>
{% endif %}
{% if timer.structure == "Engineering Complex[XL]" or timer.structure == "Sotiyo" %}
<div class="badge bg-warning">
{% translate "Sotiyo" %}
</div>
{% endif %}
{% if timer.structure == "Refinery[M]" or timer.structure == "Athanor" %}
<div class="badge bg-warning">
{% translate "Athanor" %}
</div>
{% endif %}
{% if timer.structure == "Refinery[L]" or timer.structure == "Tatara"%}
<div class="badge bg-warning">
{% translate "Tatara" %}
</div>
{% endif %}
{% if timer.structure == "Cyno Beacon" or timer.structure == "Pharolux Cyno Beacon" %}
<div class="badge bg-warning">
{% translate "Cyno Beacon" %}
</div>
{% endif %}
{% if timer.structure == "Cyno Jammer" or timer.structure == "Tenebrex Cyno Jammer" %}
<div class="badge bg-warning">
{% translate "Cyno Jammer" %}
</div>
{% endif %}
{% if timer.structure == "Jump Gate" or timer.structure == "Ansiblex Jump Gate" %}
<div class="badge bg-warning">
{% translate "Ansiblex Jump Gate" %}
</div>
{% endif %}
{% if timer.structure == "Moon Mining Cycle" %}
<div class="badge bg-success">
{% translate "Moon Mining Cycle" %}
</div>
{% endif %}
{% if timer.structure == "Other" %}
<div class="badge bg-default">
{% translate "Other" %}
</div>
{% endif %} {% endif %}
</td> </td>
<td class="text-center" nowrap> <td class="text-center" nowrap>{{ timer.eve_time | date:"Y-m-d H:i" }}</td>
{{ timer.eve_time | date:"Y-m-d H:i" }}
</td>
<td class="text-center" nowrap> <td class="text-center" nowrap>
<div id="localtime{{ timer.id }}"></div> <div id="localtime{{ timer.id }}"></div>
<div id="countdown{{ timer.id }}"></div> <div id="countdown{{ timer.id }}"></div>
</td> </td>
<td class="text-center"> <td class="text-center">{{ timer.eve_character.character_name }}</td>
{{ timer.eve_character.character_name }}
</td>
{% if perms.auth.timer_management %} {% if perms.auth.timer_management %}
<td class="text-center"> <td class="text-center">

View File

@@ -167,8 +167,8 @@ class TimerboardViewsTestCase(WebTest):
form['details'] = 'details' form['details'] = 'details'
form['system'] = 'jita' form['system'] = 'jita'
form['planet_moon'] = '4-4' form['planet_moon'] = '4-4'
form['structure'] = TimerForm.structure_choices[0][0] form['structure'] = Timer.Structure.choices[0][0]
form['objective'] = TimerForm.objective_choices[0][0] form['objective'] = Timer.Objective.choices[0][0]
form['days_left'] = 1 form['days_left'] = 1
form['hours_left'] = 2 form['hours_left'] = 2
form['minutes_left'] = 3 form['minutes_left'] = 3
@@ -206,8 +206,8 @@ class TimerboardViewsTestCase(WebTest):
form['details'] = 'detailsUNIQUE' form['details'] = 'detailsUNIQUE'
form['system'] = 'jita' form['system'] = 'jita'
form['planet_moon'] = '4-4' form['planet_moon'] = '4-4'
form['structure'] = TimerForm.structure_choices[0][0] form['structure'] = Timer.Structure.choices[0][0]
form['objective'] = TimerForm.objective_choices[0][0] form['objective'] = Timer.Objective.choices[0][0]
form['days_left'] = 1 form['days_left'] = 1
form['hours_left'] = 2 form['hours_left'] = 2
form['minutes_left'] = 3 form['minutes_left'] = 3

View File

@@ -1,5 +1,4 @@
from typing import List from typing import List, Iterable, Callable
from collections.abc import Iterable, Callable
from django.urls import include from django.urls import include
import esi.urls import esi.urls
@@ -25,8 +24,8 @@ admin.site.site_header = NAME
def urls_from_apps( def urls_from_apps(
apps_hook_functions: Iterable[Callable], public_views_allowed: list[str] apps_hook_functions: Iterable[Callable], public_views_allowed: List[str]
) -> list[URLPattern]: ) -> List[URLPattern]:
"""Return urls from apps and add default decorators.""" """Return urls from apps and add default decorators."""
url_patterns = [] url_patterns = []

View File

@@ -22,7 +22,7 @@ class ItemCounter:
DEFAULT_CACHE_TIMEOUT = 24 * 3600 DEFAULT_CACHE_TIMEOUT = 24 * 3600
def __init__( def __init__(
self, name: str, minimum: int | None = None, redis: Redis | None = None self, name: str, minimum: Optional[int] = None, redis: Optional[Redis] = None
) -> None: ) -> None:
if not name: if not name:
raise ValueError("Must define a name") raise ValueError("Must define a name")
@@ -60,6 +60,6 @@ class ItemCounter:
except ValueError: except ValueError:
pass pass
def value(self) -> int | None: def value(self) -> Optional[int]:
"""Return current value or None if not yet initialized.""" """Return current value or None if not yet initialized."""
return cache.get(self._cache_key) return cache.get(self._cache_key)

View File

@@ -1,7 +1,7 @@
PROTOCOL=https:// PROTOCOL=https://
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN% AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
DOMAIN=%DOMAIN% DOMAIN=%DOMAIN%
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.2.2 AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.3.0
# Nginx Proxy Manager # Nginx Proxy Manager
PROXY_HTTP_PORT=80 PROXY_HTTP_PORT=80

View File

@@ -1,5 +1,5 @@
FROM python:3.11-slim FROM python:3.11-slim
ARG AUTH_VERSION=v4.2.2 ARG AUTH_VERSION=v4.3.0
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION} ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
ENV AUTH_USER=allianceauth ENV AUTH_USER=allianceauth
ENV AUTH_GROUP=allianceauth ENV AUTH_GROUP=allianceauth

View File

@@ -337,7 +337,7 @@ To deactivate, click on the debug icon to switch to the debug view. Then uncheck
### AA debug config ### AA debug config
In VSC, click on Debug / Add Configuration and choose "Django". Should Django not appear as an option, make sure to first open a Django file (e.g., the local.py settings) to help VSC detect that you are using Django. In VSC, click on Debug / Add Configuration and choose "Django". Should Django not appear as an option, make sure to first open a Django file (e.g., the `local.py` settings) to help VSC detect that you are using Django.
The result should look something like this: The result should look something like this:

View File

@@ -4,9 +4,33 @@ Auto Groups allows you to automatically place users of certain states into corp
## Installation ## Installation
This is an optional app that needs to be installed. - Add `'allianceauth.eveonline.autogroups',` to `INSTALLED_APPS` in your `local.py`
To install this app add `'allianceauth.eveonline.autogroups',` to your `INSTALLED_APPS` list and run migrations. All other settings are controlled via the admin panel under the `Eve_Autogroups` section. Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
All other settings are controlled via the admin panel under the `Eve_Autogroups` section.
## Configuring a group ## Configuring a group

View File

@@ -8,7 +8,31 @@ This module is used to check the registration status of Corp members and to dete
Corp Stats requires access to the `esi-corporations.read_corporation_membership.v1` SSO scope. Update your application on the [EVE Developers site](https://developers.eveonline.com) to ensure it is available. Corp Stats requires access to the `esi-corporations.read_corporation_membership.v1` SSO scope. Update your application on the [EVE Developers site](https://developers.eveonline.com) to ensure it is available.
Add `'allianceauth.corputils',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation. - Add `'allianceauth.corputils',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Creating a Corp Stats ## Creating a Corp Stats

View File

@@ -8,7 +8,31 @@ The Fleet Activity Tracking (FAT) app allows you to track fleet participation.
Fleet Activity Tracking requires access to the `esi-location.read_location.v1`, `esi-location.read_ship_type.v1`, and `esi-universe.read_structures.v1` SSO scopes. Update your application on the [EVE Developers site](https://developers.eveonline.com) to ensure these are available. Fleet Activity Tracking requires access to the `esi-location.read_location.v1`, `esi-location.read_ship_type.v1`, and `esi-universe.read_structures.v1` SSO scopes. Update your application on the [EVE Developers site](https://developers.eveonline.com) to ensure these are available.
Add `'allianceauth.fleetactivitytracking',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation. Add `'allianceauth.fleetactivitytracking',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Permissions ## Permissions

View File

@@ -10,7 +10,31 @@ This app allows you to manage applications for multiple corporations in your all
## Installation ## Installation
Add `'allianceauth.hrapplications',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation. - Add `'allianceauth.hrapplications',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Management ## Management

View File

@@ -6,7 +6,31 @@ Fleet Operations is an app for organizing and communicating fleet schedules.
## Installation ## Installation
Add `'allianceauth.optimer',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation. - Add `'allianceauth.optimer',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Permissions ## Permissions

View File

@@ -2,9 +2,33 @@
Access to most of Alliance Auth's features is controlled by Django's permissions system. To help you secure your services, Alliance Auth provides a permission auditing tool. Access to most of Alliance Auth's features is controlled by Django's permissions system. To help you secure your services, Alliance Auth provides a permission auditing tool.
This is an optional app that needs to be installed. ## Installation
To install it add `'allianceauth.permissions_tool',` to your `INSTALLED_APPS` list in your auth project's settings file. - Add `'allianceauth.permissions_tool',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Usage ## Usage

View File

@@ -6,7 +6,31 @@ Ship Replacement helps you to organize ship replacement programs (SRP) for your
## Installation ## Installation
Add `'allianceauth.srp',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation. - Add `'allianceauth.srp',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Permissions ## Permissions

View File

@@ -6,7 +6,31 @@ Structure Timers helps you keep track of both offensive and defensive structure
## Installation ## Installation
Add `'allianceauth.timerboard',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation. - Add `'allianceauth.timerboard',` to `INSTALLED_APPS` in your `local.py`
Perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Permissions ## Permissions

View File

@@ -33,23 +33,6 @@ Our Daily Stats contain the following:
- A task to send a List of Installed Apps - A task to send a List of Installed Apps
- Each Task contains the UUID and Alliance Auth Version - Each Task contains the UUID and Alliance Auth Version
Our Celery Events contain the following:
- Unique Identifier (The UUID)
- Celery Namespace of the task e.g., allianceauth.eveonline
- Celery Task
- Task Success or Exception
- A context number for bulk tasks or sometimes a binary True/False
Our Page Views contain the following:
- Unique Identifier (The UUID)
- Page Path
- Page Title
- The locale of the users browser
- The User-Agent of the user's browser
- The Alliance Auth Version
## Why ## Why
This data allows Alliance Auth development to gather accurate statistics on our installation base, as well as how those installations are used. This data allows Alliance Auth development to gather accurate statistics on our installation base, as well as how those installations are used.

View File

@@ -4,9 +4,9 @@
It has the following key features: It has the following key features:
- Automatically grants or revokes users 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](/features/core/states) and [groups](/features/core/groups) - 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](/features/core/states) and [groups](/features/core/groups)
- Provides a central website where users can directly access web apps (e.g., SRP requests) 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"](/features/services/index)) for integrating access management with many popular external services like Discord, Mumble, Teamspeak 3, SMF and others - Includes a set of connectors (called ["services"](/features/services/index)) for integrating access management with many popular external services like Discord, Mumble, Teamspeak 3, SMF and others
@@ -14,4 +14,4 @@ It has the following key features:
- It can be easily extended with additional services and apps. Many are provided by the [community](/features/community/index). - It can be easily extended with additional services and apps. Many are provided by the [community](/features/community/index).
- Chinese, English, German and Spanish 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

View File

@@ -6,13 +6,13 @@ Discord is a web-based instant messaging client with voice. Kind of like TeamSpe
Discord is very popular amongst ad-hoc small groups and larger organizations seeking a modern technology. Alternative voice communications should be investigated for larger than small-medium groups for more advanced features. Discord is very popular amongst ad-hoc small groups and larger organizations seeking a modern technology. Alternative voice communications should be investigated for larger than small-medium groups for more advanced features.
## Setup ## Setup Auth
### Prepare Your Settings File ### Prepare Your Settings File
Make the following changes in your auth project's settings file (`local.py`): Make the following changes in your auth project's settings file (`local.py`):
- Add `'allianceauth.services.modules.discord',` to `INSTALLED_APPS` - Add `'allianceauth.services.modules.discord',` to `INSTALLED_APPS` in your `local.py`
- Append the following to the bottom of the settings file: - Append the following to the bottom of the settings file:
```python ```python
@@ -37,6 +37,34 @@ CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
You will have to add most of the values for these settings, e.g., your Discord server ID (aka guild ID), later in the setup process. You will have to add most of the values for these settings, e.g., your Discord server ID (aka guild ID), later in the setup process.
::: :::
### Preparing Auth
Before continuing, it is essential to perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Setup Discord
### Creating a Server ### Creating a Server
Navigate to the [Discord site](https://discord.com/) and register an account, or log in if you have one already. Navigate to the [Discord site](https://discord.com/) and register an account, or log in if you have one already.
@@ -67,10 +95,6 @@ Update your auth project's settings file with these pieces of information from t
- From the OAuth2 > General panel, `DISCORD_APP_SECRET` is the Client Secret - From the OAuth2 > General panel, `DISCORD_APP_SECRET` is the Client Secret
- From the Bot panel, `DISCORD_BOT_TOKEN` is the Token - From the Bot panel, `DISCORD_BOT_TOKEN` is the Token
### Preparing Auth
Before continuing, it is essential to run migrations and restart Gunicorn and Celery.
### Adding a Bot to the Server ### Adding a Bot to the Server
Once created, navigate to the "Services" page of your Alliance Auth install as the superuser account. At the top there is a big green button labeled "Link Discord Server". Click it, then from the drop-down select the server you created, and then Authorize. Once created, navigate to the "Services" page of your Alliance Auth install as the superuser account. At the top there is a big green button labeled "Link Discord Server". Click it, then from the drop-down select the server you created, and then Authorize.

View File

@@ -1,11 +1,13 @@
# Discourse # Discourse
## Prepare Your Settings ## Setup Auth
### Prepare Your Settings File
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
- Add `'allianceauth.services.modules.discourse',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.discourse',` to `INSTALLED_APPS` in your `local.py`
- Append the following to your local.py settings file: - Append the following to your `local.py` settings file:
```python ```python
# Discourse Configuration # Discourse Configuration
@@ -15,6 +17,32 @@ DISCOURSE_API_KEY = ''
DISCOURSE_SSO_SECRET = '' DISCOURSE_SSO_SECRET = ''
``` ```
### Preparing Auth
Before continuing, it is essential to perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## Install Docker ## Install Docker
```shell ```shell

View File

@@ -8,7 +8,7 @@ Mumble is a free voice chat server. While not as flashy as TeamSpeak, it has all
In your auth project's settings file (`aa-docker/conf/local.py`), do the following: In your auth project's settings file (`aa-docker/conf/local.py`), do the following:
- Add `'allianceauth.services.modules.mumble',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.mumble',` to `INSTALLED_APPS` in your `local.py`
- Append the following to your auth project's settings file: - Append the following to your auth project's settings file:
```python ```python
@@ -18,7 +18,7 @@ MUMBLE_URL = "mumble.example.com"
Add the following lines to your `.env` file Add the following lines to your `.env` file
```bash ```env
# Mumble # Mumble
MUMBLE_SUPERUSER_PASSWORD = superuser_password MUMBLE_SUPERUSER_PASSWORD = superuser_password
MUMBLE_ICESECRETWRITE = icesecretwrite MUMBLE_ICESECRETWRITE = icesecretwrite

View File

@@ -14,25 +14,38 @@ This guide is currently for Ubuntu only.
### Installing Mumble Server ### Installing Mumble Server
::::{tabs}
:::{group-tab} Ubuntu 2004, 2204
The mumble server package can be retrieved from a repository, which we need to add: The mumble server package can be retrieved from a repository, which we need to add:
::::{tabs}
:::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo apt-add-repository ppa:mumble/release sudo apt-add-repository ppa:mumble/release
``` ```
```shell :::
sudo apt-get update :::{group-tab} CentOS 7, Stream 8, Stream 9
```
sudo yum install epel-release
sudo yum update
:::
::::
Now three packages need to be installed: Now three packages need to be installed:
::::{tabs}
:::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo apt-get install python-software-properties mumble-server libqt5sql5-mysql sudo apt-get install software-properties-common mumble-server libqt5sql5-mysql
``` ```
:::
:::{group-tab} CentOS 7, Stream 8, Stream 9
sudo yum install mumble-server
::: :::
:::: ::::
@@ -58,17 +71,7 @@ pip install -r requirements.txt
## Configuring Mumble Server ## Configuring Mumble Server
The mumble server needs its own database. Open an SQL shell with `mysql -u root -p` and execute the SQL commands to create it: Mumble ships with a configuration file that needs customization. By default, it's located at `/etc/mumble-server.ini`. Open it with your favorite text editor:
```sql
CREATE DATABASE alliance_mumble CHARACTER SET utf8mb4;
```
```sql
GRANT ALL PRIVILEGES ON alliance_mumble . * TO 'allianceserver'@'localhost';
```
Mumble ships with a configuration file that needs customization. By default, its located at `/etc/mumble-server.ini`. Open it with your favorite text editor:
```shell ```shell
sudo nano /etc/mumble-server.ini sudo nano /etc/mumble-server.ini
@@ -79,15 +82,6 @@ We need to enable the ICE authenticator. Edit the following:
- `icesecretwrite=MY_CLEVER_PASSWORD`, obviously choosing a secure password - `icesecretwrite=MY_CLEVER_PASSWORD`, obviously choosing a secure password
- ensure the line containing `Ice="tcp -h 127.0.0.1 -p 6502"` is uncommented - ensure the line containing `Ice="tcp -h 127.0.0.1 -p 6502"` is uncommented
We also want to enable Mumble to use the previously created MySQL / MariaDB database, edit the following:
- uncomment the database line, and change it to `database=alliance_mumble`
- `dbDriver=QMYSQL`
- `dbUsername=allianceserver` or whatever you called the Alliance Auth MySQL user
- `dbPassword=` that users password
- `dbPort=3306`
- `dbPrefix=murmur_`
To name your root channel, uncomment and set `registerName=` to whatever cool name you want To name your root channel, uncomment and set `registerName=` to whatever cool name you want
Save and close the file. Save and close the file.
@@ -98,7 +92,7 @@ To get Mumble superuser account credentials, run the following:
sudo dpkg-reconfigure mumble-server sudo dpkg-reconfigure mumble-server
``` ```
Set the password to something youll remember and write it down. This is your superuser password and later needed to manage ACLs. Set the password to something you'll remember and write it down. This is your superuser password and later needed to manage ACLs.
Now restart the server to see the changes reflected. Now restart the server to see the changes reflected.
@@ -106,7 +100,7 @@ Now restart the server to see the changes reflected.
sudo service mumble-server restart sudo service mumble-server restart
``` ```
Thats it! Your server is ready to be connected to at example.com:64738 That's it! Your server is ready to be connected to at example.com:64738
## Configuring Mumble Authenticator ## Configuring Mumble Authenticator

View File

@@ -8,7 +8,7 @@ Openfire is a Jabber (XMPP) server.
In your auth project's settings file (`aa-docker/conf/local.py`), do the following: In your auth project's settings file (`aa-docker/conf/local.py`), do the following:
- Add `'allianceauth.services.modules.openfire',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.openfire',` to `INSTALLED_APPS` in your `local.py`
- Append the following to your auth project's settings file: - Append the following to your auth project's settings file:
```python ```python
@@ -25,7 +25,7 @@ BROADCAST_SERVICE_NAME = "broadcast"
Add the following lines to your `.env` file Add the following lines to your `.env` file
```bash ```env
# Openfire # Openfire
OPENFIRE_SECRET_KEY = superuser_password OPENFIRE_SECRET_KEY = superuser_password
BROADCAST_USER_PASSWORD = icesecretwrite BROADCAST_USER_PASSWORD = icesecretwrite

View File

@@ -4,7 +4,7 @@ Openfire is a Jabber (XMPP) server.
## Prepare Your Settings ## Prepare Your Settings
- Add `'allianceauth.services.modules.openfire',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.openfire',` to `INSTALLED_APPS` in your `local.py`
- Append the following to your auth project's settings file: - Append the following to your auth project's settings file:
```python ```python
@@ -108,7 +108,7 @@ exit;
The remainder of the setup occurs through Openfires web interface. Navigate to <http://example.com:9090>, or if youre behind CloudFlare, go straight to your servers IP:9090. The remainder of the setup occurs through Openfires web interface. Navigate to <http://example.com:9090>, or if youre behind CloudFlare, go straight to your servers IP:9090.
Select your language. I sure hope its English if youre reading this guide. Select your language.
Under Server Settings, set the Domain to `example.com` replacing it with your actual domain. Dont touch the rest. Under Server Settings, set the Domain to `example.com` replacing it with your actual domain. Dont touch the rest.

View File

@@ -12,7 +12,7 @@ phpBB3 requires PHP installed in your web server. Apache has `mod_php`, NGINX re
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
- Add `'allianceauth.services.modules.phpbb3',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.phpbb3',` to `INSTALLED_APPS` in your `local.py`
- Append the following to the bottom of the settings file: - Append the following to the bottom of the settings file:
```python ```python
@@ -25,6 +25,7 @@ DATABASES['phpbb3'] = {
'PASSWORD': 'password', 'PASSWORD': 'password',
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': '3306', 'PORT': '3306',
'OPTIONS': {'charset': 'utf8mb4'},
} }
``` ```

View File

@@ -12,7 +12,7 @@ SMF requires PHP installed in your web server. Apache has `mod_php`, NGINX requi
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
- Add `'allianceauth.services.modules.smf',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.smf',` to `INSTALLED_APPS` in your `local.py`
- Append the following to the bottom of the settings file: - Append the following to the bottom of the settings file:
```python ```python
@@ -25,6 +25,7 @@ DATABASES['smf'] = {
'PASSWORD': 'password', 'PASSWORD': 'password',
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': '3306', 'PORT': '3306',
'OPTIONS': {'charset': 'utf8mb4'},
} }
``` ```

View File

@@ -14,7 +14,7 @@ Sticking with TS3? Alright, I tried.
In your auth project's settings file (`aa-docker/conf/local.py`), do the following: In your auth project's settings file (`aa-docker/conf/local.py`), do the following:
- Add `'allianceauth.services.modules.teamspeak',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.teamspeak',` to `INSTALLED_APPS` in your `local.py`
- Append the following to your auth project's settings file: - Append the following to your auth project's settings file:
```python ```python
@@ -32,9 +32,9 @@ CELERYBEAT_SCHEDULE['run_ts3_group_update'] = {
} }
``` ```
Add the following lines to your `.env` file - Add the following lines to your `.env` file
```bash ```env
# Temspeak # Temspeak
TEAMSPEAK3_SERVERQUERY_USER = "serverquery" TEAMSPEAK3_SERVERQUERY_USER = "serverquery"
TEAMSPEAK3_SERVERQUERY_PASSWORD = "" TEAMSPEAK3_SERVERQUERY_PASSWORD = ""
@@ -152,7 +152,7 @@ If you have SSH access to the server hosting it, you need to locate the teamspea
### `520 invalid loginname or password` ### `520 invalid loginname or password`
The serverquery account login specified in local.py is incorrect. Please verify `TEAMSPEAK3_SERVERQUERY_USER` and `TEAMSPEAK3_SERVERQUERY_PASSWORD`. The [installation section](#update-settings) describes where to get them. The serverquery account login specified in `local.py` is incorrect. Please verify `TEAMSPEAK3_SERVERQUERY_USER` and `TEAMSPEAK3_SERVERQUERY_PASSWORD`. The [installation section](#update-settings) describes where to get them.
### `2568 insufficient client permissions` ### `2568 insufficient client permissions`

View File

@@ -14,7 +14,7 @@ Sticking with TS3? Alright, I tried.
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
- Add `'allianceauth.services.modules.teamspeak3',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.teamspeak3',` to `INSTALLED_APPS` in your `local.py`
- Append the following to the bottom of the settings file: - Append the following to the bottom of the settings file:
```python ```python
@@ -170,7 +170,7 @@ If you have SSH access to the server hosting it, you need to locate the teamspea
### `520 invalid loginname or password` ### `520 invalid loginname or password`
The serverquery account login specified in local.py is incorrect. Please verify `TEAMSPEAK3_SERVERQUERY_USER` and `TEAMSPEAK3_SERVERQUERY_PASSWORD`. The [installation section](#update-settings) describes where to get them. The serverquery account login specified in `local.py` is incorrect. Please verify `TEAMSPEAK3_SERVERQUERY_USER` and `TEAMSPEAK3_SERVERQUERY_PASSWORD`. The [installation section](#update-settings) describes where to get them.
### `2568 insufficient client permissions` ### `2568 insufficient client permissions`

View File

@@ -4,12 +4,14 @@
[XenForo](https://xenforo.com/) is a popular, paid forum. This guide will assume that you already have XenForo installed with a valid license (please keep in mind that XenForo is not free nor open-source, therefore, you need to purchase a license first). If you come across any problems related with the installation of XenForo please contact their support service. [XenForo](https://xenforo.com/) is a popular, paid forum. This guide will assume that you already have XenForo installed with a valid license (please keep in mind that XenForo is not free nor open-source, therefore, you need to purchase a license first). If you come across any problems related with the installation of XenForo please contact their support service.
## Prepare Your Settings ## Setup Auth
### Prepare Your Settings
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
- Add `'allianceauth.services.modules.xenforo',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.xenforo',` to `INSTALLED_APPS` in your `local.py`
- Append the following to your local.py settings file: - Append the following to your `local.py` settings file:
```python ```python
# XenForo Configuration # XenForo Configuration
@@ -18,6 +20,32 @@ XENFORO_DEFAULT_GROUP = 0
XENFORO_APIKEY = 'yourapikey' XENFORO_APIKEY = 'yourapikey'
``` ```
### Preparing Auth
Before continuing, it is essential to perform Django Maintenance and restart our Web Service and Workers.
::::{tabs}
:::{group-tab} Bare Metal
```shell
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart myauth:
```
:::
:::{group-tab} Containerized
```shell
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
auth migrate
auth collectstatic
```
:::
::::
## XenAPI ## XenAPI
By default, XenForo does not support any kind of API, however, there is a third-party package called [XenAPI](https://github.com/Contex/XenAPI) which provides a simple REST interface by which we can access XenForo's functions to create and edit users. By default, XenForo does not support any kind of API, however, there is a third-party package called [XenAPI](https://github.com/Contex/XenAPI) which provides a simple REST interface by which we can access XenForo's functions to create and edit users.

View File

@@ -29,7 +29,7 @@ Replace your docker-compose.yml with the contents of <https://gitlab.com/allianc
V3.x installs likely used a dedicated database for Nginx Proxy Manager, you can either setup NPM again without a database, or uncomment the sections noted to maintain this configuration V3.x installs likely used a dedicated database for Nginx Proxy Manager, you can either setup NPM again without a database, or uncomment the sections noted to maintain this configuration
```docker ```docker-compose
proxy: proxy:
... ...
# Uncomment this section to use a dedicated database for Nginx Proxy Manager # Uncomment this section to use a dedicated database for Nginx Proxy Manager
@@ -64,7 +64,7 @@ V3.x installs likely used a dedicated database for Nginx Proxy Manager, you can
You will need to add some entries to your .env file You will need to add some entries to your .env file
```bash ```env
AA_DB_CHARSET=utf8mb4 AA_DB_CHARSET=utf8mb4
GF_SECURITY_ADMIN_USERNAME=admin GF_SECURITY_ADMIN_USERNAME=admin
``` ```

View File

@@ -14,7 +14,7 @@ Alliance Auth can be installed on any in-support *nix operating system.
Our install documentation targets the following operating systems. Our install documentation targets the following operating systems.
- Ubuntu 20.04 - Ubuntu 20.04 - Not Recommended for new installs
- Ubuntu 22.04 - Ubuntu 22.04
- Centos 7 - Centos 7
- CentOS Stream 8 - CentOS Stream 8
@@ -27,7 +27,7 @@ To install on your favorite flavour of Linux, identify and install equivalent pa
It is recommended to ensure your OS is fully up-to-date before proceeding. We may also add Package Repositories here, used later in the documentation. It is recommended to ensure your OS is fully up-to-date before proceeding. We may also add Package Repositories here, used later in the documentation.
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo apt-get update sudo apt-get update
@@ -70,7 +70,7 @@ Install Python 3.11 and related tools on your system.
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo add-apt-repository ppa:deadsnakes/ppa sudo add-apt-repository ppa:deadsnakes/ppa
@@ -128,7 +128,7 @@ sudo make altinstall
It's recommended to use a database service instead of SQLite. Many options are available, but this guide will use MariaDB 10.11 It's recommended to use a database service instead of SQLite. Many options are available, but this guide will use MariaDB 10.11
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=20.04+%22focal%22&v=10.11&r_m=osuosl> to add the MariaDB repository to your host. Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=20.04+%22focal%22&v=10.11&r_m=osuosl> to add the MariaDB repository to your host.
```shell ```shell
@@ -164,7 +164,7 @@ sudo dnf install mariadb mariadb-server mariadb-devel
:::::{important} :::::{important}
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
If you don't plan on running the database on the same server as auth you still need to install the `libmysqlclient-dev` package If you don't plan on running the database on the same server as auth you still need to install the `libmysqlclient-dev` package
::: :::
:::{group-tab} CentOS 7 :::{group-tab} CentOS 7
@@ -185,7 +185,7 @@ A few extra utilities are also required for the installation of packages.
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
@@ -279,13 +279,13 @@ mysql_secure_installation
### User Account ### User Account
For security and permissions, its highly recommended you create a separate user to install auth under. Do not log in as this account. For security and permissions, it's highly recommended you create a separate user to install auth under. Do not log in as this account.
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo adduser --disabled-login allianceserver sudo adduser --disabled-login allianceserver --shell /bin/bash
``` ```
::: :::
@@ -497,7 +497,7 @@ exit
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo apt-get install supervisor sudo apt-get install supervisor
@@ -554,7 +554,7 @@ sudo systemctl start supervisord.service
Once installed, it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running. Once installed, it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisor/conf.d/myauth.conf ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisor/conf.d/myauth.conf

View File

@@ -10,7 +10,7 @@ If you're using a small VPS to host services with very limited memory, consider
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
apt-get install apache2 apt-get install apache2
@@ -50,7 +50,7 @@ CentOS 7, Stream 8, Stream 9
Apache needs to be able to read the folder containing your auth project's static files. Apache needs to be able to read the folder containing your auth project's static files.
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
chown -R www-data:www-data /var/www/myauth/static chown -R www-data:www-data /var/www/myauth/static
@@ -87,7 +87,7 @@ Apache serves sites through defined virtual hosts. These are located in `/etc/ap
A virtual host for auth needs only proxy requests to your WSGI server (Gunicorn if you followed the installation guide) and serve static files. Examples can be found below. Create your config in its own file e.g. `myauth.conf` A virtual host for auth needs only proxy requests to your WSGI server (Gunicorn if you followed the installation guide) and serve static files. Examples can be found below. Create your config in its own file e.g. `myauth.conf`
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
To proxy and modify headers a few mods need to be enabled. To proxy and modify headers a few mods need to be enabled.
```shell ```shell
@@ -122,7 +122,7 @@ a2dissite 000-default.conf
## Sample Config File ## Sample Config File
```apacheconf ```ini
<VirtualHost *:80> <VirtualHost *:80>
ServerName auth.example.com ServerName auth.example.com

View File

@@ -42,7 +42,7 @@ You will need to have [Gunicorn](gunicorn.md) or some other WSGI server setup fo
## Install ## Install
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo apt-get install nginx sudo apt-get install nginx

View File

@@ -25,7 +25,7 @@ sudo dnf install python39 python39-devel
::: :::
::::{tabs} ::::{tabs}
:::{group-tab} Ubuntu 2004, 2204 :::{group-tab} Ubuntu 2004, 2204, 2404
```shell ```shell
sudo add-apt-repository ppa:deadsnakes/ppa sudo add-apt-repository ppa:deadsnakes/ppa

View File

@@ -4,10 +4,25 @@
Your auth project is just a regular Django project - you can add in [other Django apps](https://djangopackages.org/) as desired. Most come with dedicated setup guides, but here is the general procedure: Your auth project is just a regular Django project - you can add in [other Django apps](https://djangopackages.org/) as desired. Most come with dedicated setup guides, but here is the general procedure:
1. add `'appname',` to your `INSTALLED_APPS` setting in `local.py` ::::{tabs}
2. run `python manage.py migrate` :::{group-tab} Bare Metal
3. run `python manage.py collectstatic --noinput`
4. restart AA with `supervisorctl restart myauth:` 1. Add `'appname',` to `INSTALLED_APPS` setting in `local.py`
1. Run Migrations `python manage.py migrate`
1. Collect Static Files `python manage.py collectstatic --noinput`
1. Restart our Web Service and Workers `supervisorctl restart myauth:`
:::
:::{group-tab} Containerized
1. Add `'appname',` to `INSTALLED_APPS` setting in `local.py`
1. Restart our Web Service and Workers `docker compose --env-file=.env up -d`
1. Enter a Docker Container `docker compose exec allianceauth_gunicorn bash`
1. Run Migrations `auth migrate`
1. Collect Static Files `auth collectstatic`
:::
::::
## Removing Apps ## Removing Apps
@@ -25,20 +40,38 @@ First, we want to remove the app related tables from the database.
Let's first try the automatic approach by running the following command: Let's first try the automatic approach by running the following command:
::::{tabs}
:::{group-tab} Bare Metal
```shell ```shell
python manage.py migrate appname zero python manage.py migrate appname zero
``` ```
:::
:::{group-tab} Containerized
```shell
docker compose exec allianceauth_gunicorn bash
auth migrate appname zero
```
:::
::::
If that works, you'll get a confirmation message. If that works, you'll get a confirmation message.
If that did not work, and you got error messages, you will need to remove the tables manually. This is pretty common btw, because many apps use sophisticated table setups, which cannot be removed automatically by Django. If that did not work, and you got error messages, you will need to remove the tables manually.
> This is pretty common, because many apps use sophisticated table setups, which cannot be removed automatically by Django.
#### Manual table removal #### Manual table removal
::::{tabs}
:::{group-tab} Bare Metal
First, tell Django that these migrations are no longer in effect (note the additional `--fake`): First, tell Django that these migrations are no longer in effect (note the additional `--fake`):
```shell ```shell
python manage.py migrate appname zero --fake python manage.py appname zero --fake
``` ```
Then, open the mysql tool and connect to your Alliance Auth database: Then, open the mysql tool and connect to your Alliance Auth database:
@@ -75,6 +108,53 @@ SET FOREIGN_KEY_CHECKS=1;
exit; exit;
``` ```
:::
:::{group-tab} Containerized
First, tell Django that these migrations are no longer in effect (note the additional `--fake`):
```shell
auth migrate appname zero --fake
```
Here we need to swap containers, if you are still inside allianceauth_gunicorn, exit with `exit`
```shell
docker compose exec auth_mysql bash
sudo mysql -u root
use alliance_auth;
```
Next, disable foreign key check. This makes it much easier to drop tables in any order.
```shell
SET FOREIGN_KEY_CHECKS=0;
```
Then get a list of all tables. All tables belonging to the app in question will start with `appname_`.
```shell
show tables;
```
Now, drop the tables from the app one by one like so:
```shell
drop table appname_model_1;
drop table appname_model_2;
...
```
And finally, but very importantly, re-enable foreign key checks again and then exit:
```shell
SET FOREIGN_KEY_CHECKS=1;
exit;
```
:::
::::
### Step 2 - Remove the app from Alliance Auth ### Step 2 - Remove the app from Alliance Auth
Once the tables have been removed, you can remove the app from Alliance Auth. This is done by removing the applabel from the `INSTALLED_APPS` list in your local settings file. Once the tables have been removed, you can remove the app from Alliance Auth. This is done by removing the applabel from the `INSTALLED_APPS` list in your local settings file.

View File

@@ -9,7 +9,7 @@ Tuning usually has benefits and costs and should only be performed by experience
:::{toctree} :::{toctree}
:maxdepth: 1 :maxdepth: 1
web gunicorn
celery celery
redis redis
python python

View File

@@ -99,7 +99,7 @@ Replace the nginx service in your docker-compose as follows
Modify your nginx.conf as follows Modify your nginx.conf as follows
```nginx ```conf
load_module modules/ngx_http_brotli_static_module.so; load_module modules/ngx_http_brotli_static_module.so;
load_module modules/ngx_http_brotli_filter_module.so; load_module modules/ngx_http_brotli_filter_module.so;
... ...

View File

@@ -15,7 +15,7 @@ license = { file = "LICENSE" }
authors = [ authors = [
{ name = "Alliance Auth", email = "adarnof@gmail.com" }, { name = "Alliance Auth", email = "adarnof@gmail.com" },
] ]
requires-python = ">=3.10" requires-python = ">=3.8"
classifiers = [ classifiers = [
"Environment :: Web Environment", "Environment :: Web Environment",
"Framework :: Celery", "Framework :: Celery",
@@ -26,6 +26,8 @@ classifiers = [
"Operating System :: POSIX :: Linux", "Operating System :: POSIX :: Linux",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
@@ -41,13 +43,14 @@ dependencies = [
"beautifulsoup4", "beautifulsoup4",
"celery<6,>=5.2", "celery<6,>=5.2",
"celery-once>=3.0.1", "celery-once>=3.0.1",
"django<5.2,>=5.1", "django<5,>=4.2",
"django-bootstrap-form", "django-bootstrap-form",
"django-bootstrap5>=23.3", "django-bootstrap5>=23.3",
"django-celery-beat>=2.7", "django-celery-beat>=2.3",
"django-esi>=5", "django-esi>=5",
"django-redis>=5.2", "django-redis>=5.2",
"django-registration<3.4,>=3.3", "django-registration<3.4,>=3.3",
"django-solo",
"django-sortedm2m", "django-sortedm2m",
"dnspython", "dnspython",
"mysqlclient>=2.1", "mysqlclient>=2.1",

View File

@@ -1,4 +1,5 @@
import os import os
from celery import Celery from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
@@ -12,6 +13,10 @@ app = Celery('devauth')
# the configuration object to child processes. # the configuration object to child processes.
app.config_from_object('django.conf:settings') app.config_from_object('django.conf:settings')
# Automatically try to establish the connection to the AMQP broker on
# Celery startup if it is unavailable.
app.conf.broker_connection_retry_on_startup = True
# setup priorities ( 0 Highest, 9 Lowest ) # setup priorities ( 0 Highest, 9 Lowest )
app.conf.broker_transport_options = { app.conf.broker_transport_options = {
'priority_steps': list(range(10)), # setup que to have 10 steps 'priority_steps': list(range(10)), # setup que to have 10 steps

View File

@@ -2,17 +2,18 @@
isolated_build = true isolated_build = true
skipsdist = true skipsdist = true
usedevelop = true usedevelop = true
envlist = py{310,311,312,313}-{all,core}, docs envlist = py{38,39,310,311,312}-{all,core}, docs
[testenv] [testenv]
setenv = setenv =
all: DJANGO_SETTINGS_MODULE = tests.settings_all all: DJANGO_SETTINGS_MODULE = tests.settings_all
core: DJANGO_SETTINGS_MODULE = tests.settings_core core: DJANGO_SETTINGS_MODULE = tests.settings_core
basepython = basepython =
py38: python3.8
py39: python3.9
py310: python3.10 py310: python3.10
py311: python3.11 py311: python3.11
py312: python3.12 py312: python3.12
py313: python3.13
deps= deps=
coverage coverage
install_command = pip install -e ".[test]" -U {opts} {packages} install_command = pip install -e ".[test]" -U {opts} {packages}
@@ -24,7 +25,7 @@ commands =
[testenv:docs] [testenv:docs]
description = invoke sphinx-build to build the HTML docs description = invoke sphinx-build to build the HTML docs
basepython = python3.12 basepython = python3.11
install_command = pip install -e ".[docs]" -U {opts} {packages} install_command = pip install -e ".[docs]" -U {opts} {packages}
commands = commands =
sphinx-build -T -E -b html -d "{toxworkdir}/docs_doctree" -D language=en docs "{toxworkdir}/docs_out" {posargs} sphinx-build -T -E -b html -d "{toxworkdir}/docs_doctree" -D language=en docs "{toxworkdir}/docs_out" {posargs}