Compare commits

..

No commits in common. "master" and "v4.0.0a3" have entirely different histories.

492 changed files with 14265 additions and 43011 deletions

View File

@ -19,6 +19,5 @@ exclude_lines =
if __name__ == .__main__.:
def __repr__
raise AssertionError
if TYPE_CHECKING:
ignore_errors = True

1
.gitignore vendored
View File

@ -73,4 +73,3 @@ celerybeat-schedule
.flake8
.pylintrc
Makefile
alliance_auth.sqlite3

View File

@ -25,12 +25,12 @@ before_script:
pre-commit-check:
<<: *only-default
stage: pre-commit
image: python:3.11-bookworm
# variables:
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
# cache:
# paths:
# - ${PRE_COMMIT_HOME}
image: python:3.11-bullseye
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
paths:
- ${PRE_COMMIT_HOME}
script:
- pip install pre-commit
- pre-commit run --all-files
@ -53,7 +53,7 @@ secret_detection:
test-3.8-core:
<<: *only-default
image: python:3.8-bookworm
image: python:3.8-bullseye
script:
- tox -e py38-core
artifacts:
@ -65,7 +65,7 @@ test-3.8-core:
test-3.9-core:
<<: *only-default
image: python:3.9-bookworm
image: python:3.9-bullseye
script:
- tox -e py39-core
artifacts:
@ -77,7 +77,7 @@ test-3.9-core:
test-3.10-core:
<<: *only-default
image: python:3.10-bookworm
image: python:3.10-bullseye
script:
- tox -e py310-core
artifacts:
@ -89,7 +89,7 @@ test-3.10-core:
test-3.11-core:
<<: *only-default
image: python:3.11-bookworm
image: python:3.11-bullseye
script:
- tox -e py311-core
artifacts:
@ -99,9 +99,22 @@ test-3.11-core:
coverage_format: cobertura
path: coverage.xml
test-pvpy-core:
<<: *only-default
image: pypy:3.9-bullseye
script:
- tox -e pypy-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-3.12-core:
<<: *only-default
image: python:3.12-bookworm
image: python:3.12-rc-bullseye
script:
- tox -e py312-core
artifacts:
@ -110,10 +123,11 @@ test-3.12-core:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-3.8-all:
<<: *only-default
image: python:3.8-bookworm
image: python:3.8-bullseye
script:
- tox -e py38-all
artifacts:
@ -125,7 +139,7 @@ test-3.8-all:
test-3.9-all:
<<: *only-default
image: python:3.9-bookworm
image: python:3.9-bullseye
script:
- tox -e py39-all
artifacts:
@ -137,7 +151,7 @@ test-3.9-all:
test-3.10-all:
<<: *only-default
image: python:3.10-bookworm
image: python:3.10-bullseye
script:
- tox -e py310-all
artifacts:
@ -149,7 +163,7 @@ test-3.10-all:
test-3.11-all:
<<: *only-default
image: python:3.11-bookworm
image: python:3.11-bullseye
script:
- tox -e py311-all
artifacts:
@ -160,9 +174,22 @@ test-3.11-all:
path: coverage.xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
test-pvpy-all:
<<: *only-default
image: pypy:3.9-bullseye
script:
- tox -e pypy-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-3.12-all:
<<: *only-default
image: python:3.12-bookworm
image: python:3.12-rc-bullseye
script:
- tox -e py312-all
artifacts:
@ -171,10 +198,11 @@ test-3.12-all:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
build-test:
stage: test
image: python:3.11-bookworm
image: python:3.11-bullseye
before_script:
- python -m pip install --upgrade pip
@ -193,13 +221,13 @@ build-test:
test-docs:
<<: *only-default
image: python:3.11-bookworm
image: python:3.11-bullseye
script:
- tox -e docs
- tox -e docs
deploy_production:
stage: deploy
image: python:3.11-bookworm
image: python:3.11-bullseye
before_script:
- python -m pip install --upgrade pip
@ -231,7 +259,16 @@ build-image:
docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --use --name new-builder
docker buildx build . --tag $IMAGE_TAG --tag $CURRENT_TAG --tag $MINOR_TAG --tag $MAJOR_TAG --tag $LATEST_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
docker buildx build .
--tag $IMAGE_TAG
--tag $CURRENT_TAG
--tag $MINOR_TAG
--tag $MAJOR_TAG
--tag $LATEST_TAG
--file docker/Dockerfile
--platform linux/amd64,linux/arm64
--push
--build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
rules:
- if: $CI_COMMIT_TAG
when: delayed
@ -251,7 +288,12 @@ build-image-dev:
docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --use --name new-builder
docker buildx build . --tag $IMAGE_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
docker buildx build .
--tag $IMAGE_TAG
--file docker/Dockerfile
--platform linux/amd64,linux/arm64
--push
--build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
rules:
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
when: manual
@ -272,7 +314,12 @@ build-image-mr:
docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --use --name new-builder
docker buildx build . --tag $IMAGE_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
docker buildx build .
--tag $IMAGE_TAG
--file docker/Dockerfile
--platform linux/amd64,linux/arm64
--push
--build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual

View File

@ -3,41 +3,9 @@
# Update this file:
# pre-commit autoupdate
# Set the default language versions for the hooks
default_language_version:
python: python3 # Force all Python hooks to use Python 3
node: 22.12.0 # Force all Node hooks to use Node 22.12.0
# Globally exclude files
# https://pre-commit.com/#top_level-exclude
exclude: |
(?x)(
LICENSE|
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json|
static/(.*)/libs/
)
repos:
# Code Upgrades
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.25.0
hooks:
- id: django-upgrade
args: [--target-version=4.2]
# Formatting
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v4.4.0
hooks:
# Identify invalid files
- id: check-ast
@ -45,51 +13,73 @@ repos:
- id: check-json
- id: check-toml
- id: check-xml
# git checks
- id: check-merge-conflict
- id: check-added-large-files
args: [--maxkb=1000]
args: [ --maxkb=1000 ]
- id: detect-private-key
- id: check-case-conflict
# Python checks
# - id: check-docstring-first
- id: debug-statements
# - id: requirements-txt-fixer
- id: fix-encoding-pragma
args: [--remove]
args: [ --remove ]
- id: fix-byte-order-marker
# General quality checks
- id: mixed-line-ending
args: [--fix=lf]
args: [ --fix=lf ]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
args: [ --markdown-linebreak-ext=md ]
exclude: |
(?x)(
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json
)
- id: check-executables-have-shebangs
- id: end-of-file-fixer
exclude: |
(?x)(
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json
)
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 3.2.1
rev: 2.7.2
hooks:
- id: editorconfig-checker
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0
exclude: |
(?x)(
LICENSE|
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
\.po|
\.mo|
swagger\.json
)
- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
hooks:
- id: markdownlint
language: node
args:
- --disable=MD013
# Infrastructure
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.6.0
- id: pyupgrade
args: [ --py38-plus ]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.14.0
hooks:
- id: pyproject-fmt
name: pyproject.toml formatter
description: "Format the pyproject.toml file."
args:
- --indent=4
additional_dependencies:
- tox==4.24.1 # https://github.com/tox-dev/tox/releases/latest
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
- id: django-upgrade
args: [--target-version=4.2]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.3.0
hooks:
- id: validate-pyproject
name: Validate pyproject.toml
description: "Validate the pyproject.toml file."
- id: setup-cfg-fmt
args: [ --include-version-classifiers ]

View File

@ -7,14 +7,11 @@ version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
os: ubuntu-20.04
apt_packages:
- redis
tools:
python: "3.11"
jobs:
post_system_dependencies:
- redis-server --daemonize yes
python: "3.8"
# Build documentation in the docs/ directory with Sphinx
sphinx:
@ -23,7 +20,7 @@ sphinx:
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Python requirements required to build your docs
# Optionally set the version of Python and requirements required to build your docs
python:
install:
- method: pip

10
.tx/config Normal file
View File

@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
lang_map = zh-Hans: zh_Hans
[o:alliance-auth:p:alliance-auth:r:django-po]
file_filter = allianceauth/locale/<lang>/LC_MESSAGES/django.po
source_file = allianceauth/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO
minimum_perc = 0

View File

@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
lang_map = zh-Hans:zh_Hans
[alliance-auth.django-po]
file_filter = allianceauth/locale/<lang>/LC_MESSAGES/django.po
minimum_perc = 0
source_file = allianceauth/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO

View File

@ -1,10 +0,0 @@
filters:
- filter_type: file
file_format: PO
source_file: allianceauth/locale/en/LC_MESSAGES/django.po
source_language: en
translation_files_expression: allianceauth/locale/<lang>/LC_MESSAGES/django.po
settings:
language_mapping:
zh-Hans: zh_Hans

View File

@ -1,56 +1,50 @@
# Alliance Auth
[![License](https://img.shields.io/badge/license-GPLv2-green)](https://pypi.org/project/allianceauth/)
[![Python Versions](https://img.shields.io/pypi/pyversions/allianceauth)](https://pypi.org/project/allianceauth/)
[![Django Versions](https://img.shields.io/pypi/djversions/allianceauth?label=django)](https://pypi.org/project/allianceauth/)
[![Stable AA Version](https://img.shields.io/pypi/v/allianceauth?label=release)](https://pypi.org/project/allianceauth/)
[![Pipeline Status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](https://allianceauth.readthedocs.io/?badge=latest)
[![Test Coverage Report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![license](https://img.shields.io/badge/license-GPLv2-green)](https://pypi.org/project/allianceauth/)
[![python](https://img.shields.io/pypi/pyversions/allianceauth)](https://pypi.org/project/allianceauth/)
[![django](https://img.shields.io/pypi/djversions/allianceauth?label=django)](https://pypi.org/project/allianceauth/)
[![version](https://img.shields.io/pypi/v/allianceauth?label=release)](https://pypi.org/project/allianceauth/)
[![pipeline status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest)
[![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk)
A flexible authentication platform for EVE Online to help in-game organizations manage access to applications and services. AA provides both, a stable core, and a robust framework for community development and custom applications.
An auth system for EVE Online to help in-game organizations manage online service access.
## Content
- [Overview](#overview)
- [Documentation](https://allianceauth.rtfd.io)
- [Documentation](http://allianceauth.rtfd.io)
- [Support](#support)
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
- [Developer Team](#development-team)
- [Developer Team](#developer-team)
- [Contributing](#contributing)
## Overview
Alliance Auth (AA) is a platform that helps Eve Online organizations efficiently manage access to applications and services.
Alliance Auth (AA) is a web site that helps Eve Online organizations efficiently manage access to applications and services.
Main features:
- Automatically grants or revokes user access to external services (e.g.: Discord, Mumble) based on the user's current membership to [a variety of EVE Online affiliation](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
- Automatically grants or revokes user access to external services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
- Includes a set of connectors (called ["Services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
- Includes a set of web [Apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
- Includes a set of web [apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr:, Russian :flag_ru:, Italian :flag_it:, French :flag_fr:, Japanese :flag_jp: and Ukrainian :flag_ua: Localization
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](https://allianceauth.rtfd.io).
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
## Screenshot
Here is an example of the Alliance Auth web site with a mixture of Services, Apps and Community Creations enabled:
Here is an example of the Alliance Auth web site with some plug-ins apps and services enabled:
### Flatly Theme
![Flatly Theme](docs/_static/images/promotion/SampleInstallation-Flatly.png)
### Darkly Theme
![Darkly Theme](docs/_static/images/promotion/SampleInstallation-Darkly.png)
![screenshot](https://i.imgur.com/2tnX9kD.png)
## Support

View File

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

View File

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

View File

@ -1,8 +1,6 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class AnalyticsConfig(AppConfig):
name = 'allianceauth.analytics'
label = 'analytics'
verbose_name = _('Analytics')

View File

@ -1,17 +0,0 @@
# Generated by Django 4.2.16 on 2024-12-11 02:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('analytics', '0009_remove_analyticstokens_ignore_paths_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='analyticsidentifier',
options={'verbose_name': 'Analytics Identifier'},
),
]

View File

@ -1,19 +1,23 @@
from typing import Literal
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from solo.models import SingletonModel
from uuid import uuid4
class AnalyticsIdentifier(SingletonModel):
class AnalyticsIdentifier(models.Model):
identifier = models.UUIDField(default=uuid4, editable=False)
identifier = models.UUIDField(
default=uuid4,
editable=False)
def __str__(self) -> Literal['Analytics Identifier']:
return "Analytics Identifier"
class Meta:
verbose_name = "Analytics Identifier"
def save(self, *args, **kwargs):
if not self.pk and AnalyticsIdentifier.objects.exists():
# Force a single object
raise ValidationError('There is can be only one \
AnalyticsIdentifier instance')
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
return super().save(*args, **kwargs)
class AnalyticsTokens(models.Model):

View File

@ -5,13 +5,10 @@ from django.apps import apps
from celery import shared_task
from .models import AnalyticsTokens, AnalyticsIdentifier
from .utils import (
existence_baremetal_or_docker,
install_stat_addons,
install_stat_tokens,
install_stat_users)
from allianceauth import __version__
logger = logging.getLogger(__name__)
BASE_URL = "https://www.google-analytics.com"
@ -68,8 +65,8 @@ def analytics_event(namespace: str,
value=value).apply_async(priority=9)
@shared_task
def analytics_daily_stats() -> None:
@shared_task()
def analytics_daily_stats():
"""Celery Task: Do not call directly
Gathers a series of daily statistics
@ -78,7 +75,6 @@ def analytics_daily_stats() -> None:
users = install_stat_users()
tokens = install_stat_tokens()
addons = install_stat_addons()
existence_type = existence_baremetal_or_docker()
logger.debug("Running Daily Analytics Upload")
analytics_event(namespace='allianceauth.analytics',
@ -86,11 +82,6 @@ def analytics_daily_stats() -> None:
label='existence',
value=1,
event_type='Stats')
analytics_event(namespace='allianceauth.analytics',
task='send_install_stats',
label=existence_type,
value=1,
event_type='Stats')
analytics_event(namespace='allianceauth.analytics',
task='send_install_stats',
label='users',
@ -106,42 +97,16 @@ def analytics_daily_stats() -> None:
label='addons',
value=addons,
event_type='Stats')
for appconfig in apps.get_app_configs():
if appconfig.label in [
"django_celery_beat",
"bootstrapform",
"messages",
"sessions",
"auth",
"staticfiles",
"users",
"addons",
"admin",
"humanize",
"contenttypes",
"sortedm2m",
"django_bootstrap5",
"tokens",
"authentication",
"services",
"framework",
"notifications"
"eveonline",
"navhelper",
"analytics",
"menu",
"theme"
]:
pass
else:
analytics_event(namespace='allianceauth.analytics',
task='send_extension_stats',
label=appconfig.label,
value=1,
event_type='Stats')
analytics_event(namespace='allianceauth.analytics',
task='send_extension_stats',
label=appconfig.label,
value=1,
event_type='Stats')
@shared_task
@shared_task()
def send_ga_tracking_celery_event(
measurement_id: str,
secret: str,
@ -171,10 +136,10 @@ def send_ga_tracking_celery_event(
}
payload = {
'client_id': AnalyticsIdentifier.get_solo().identifier.hex,
'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex,
"user_properties": {
"allianceauth_version": {
"value": __version__
"value": "allianceauth_version"
}
},
'non_personalized_ads': True,

View File

@ -1,8 +1,9 @@
from allianceauth.analytics.models import AnalyticsIdentifier
from django.core.exceptions import ValidationError
from django.test.testcases import TestCase
from uuid import uuid4
from uuid import UUID, uuid4
# Identifiers
@ -13,4 +14,14 @@ uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
class TestAnalyticsIdentifier(TestCase):
def test_identifier_random(self):
self.assertNotEqual(AnalyticsIdentifier.get_solo(), uuid4)
self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4)
def test_identifier_singular(self):
AnalyticsIdentifier.objects.all().delete()
AnalyticsIdentifier.objects.create(identifier=uuid_1)
# Yeah i have multiple asserts here, they all do the same thing
with self.assertRaises(ValidationError):
AnalyticsIdentifier.objects.create(identifier=uuid_2)
self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
self.assertEqual(AnalyticsIdentifier.objects.get(
pk=1).identifier, UUID(uuid_1))

View File

@ -1,4 +1,3 @@
import os
from django.apps import apps
from allianceauth.authentication.models import User
from esi.models import Token
@ -35,16 +34,3 @@ def install_stat_addons() -> int:
The Number of Installed Apps"""
addons = len(list(apps.get_app_configs()))
return addons
def existence_baremetal_or_docker() -> str:
"""Checks the Installation Type of an install
Returns
-------
str
existence_baremetal or existence_docker"""
docker_tag = os.getenv('AA_DOCKER_TAG')
if docker_tag:
return "existence_docker"
return "existence_baremetal"

View File

@ -1,8 +1,30 @@
from django.apps import AppConfig
from django.core.checks import Warning, Error, register
class AllianceAuthConfig(AppConfig):
name = 'allianceauth'
def ready(self) -> None:
import allianceauth.checks # noqa
@register()
def check_settings(app_configs, **kwargs):
from django.conf import settings
errors = []
if hasattr(settings, "SITE_URL"):
if settings.SITE_URL[-1] == "/":
errors.append(Warning(
"'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth."))
else:
errors.append(Error(
"No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules."))
if hasattr(settings, "CSRF_TRUSTED_ORIGINS"):
if hasattr(settings, "SITE_URL"):
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
errors.append(Warning(
"'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified."))
else:
errors.append(Error(
"No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified"))
return errors

View File

@ -1,12 +1,10 @@
from django.apps import AppConfig
from django.core.checks import register, Tags
from django.utils.translation import gettext_lazy as _
class AuthenticationConfig(AppConfig):
name = "allianceauth.authentication"
label = "authentication"
verbose_name = _("Authentication")
def ready(self):
from allianceauth.authentication import checks, signals # noqa: F401

View File

@ -1,6 +1,6 @@
from allianceauth.hooks import DashboardItemHook
from allianceauth import hooks
from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin
from .views import dashboard_characters, dashboard_groups, dashboard_admin
class UserCharactersHook(DashboardItemHook):
@ -26,15 +26,6 @@ class AdminHook(DashboardItemHook):
DashboardItemHook.__init__(
self,
dashboard_admin,
1
)
class ESICheckHook(DashboardItemHook):
def __init__(self):
DashboardItemHook.__init__(
self,
dashboard_esi_check,
0
)
@ -52,8 +43,3 @@ def register_groups_hook():
@hooks.register('dashboard_hook')
def register_admin_hook():
return AdminHook()
@hooks.register('dashboard_hook')
def register_esi_hook():
return ESICheckHook()

View File

@ -2,6 +2,7 @@ import logging
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, Permission
from django.contrib import messages
from .models import UserProfile, CharacterOwnership, OwnershipRecord
@ -40,7 +41,9 @@ class StateBackend(ModelBackend):
if ownership.user.profile.main_character:
if ownership.user.profile.main_character.character_id == token.character_id:
return ownership.user
else: # this is an alt, enforce main only.
else: ## this is an alt, enforce main only.
if request:
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account.")
return None
else:
logger.debug(f'{token.character_name} has changed ownership. Creating new user account.')
@ -63,7 +66,9 @@ class StateBackend(ModelBackend):
user = records[0].user
if user.profile.main_character:
if user.profile.main_character.character_id != token.character_id:
# this is an alt, enforce main only due to trust issues in SSO.
## this is an alt, enforce main only due to trust issues in SSO.
if request:
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.")
return None
token.user = user

View File

@ -1,12 +0,0 @@
from django.utils.translation import gettext_lazy as _
# Overide ESI messages in the dashboard widget
# when the returned messages are not helpful or out of date
ESI_ERROR_MESSAGE_OVERRIDES = {
420: _("This software has exceeded the error limit for ESI. "
"If you are a user, please contact the maintainer of this software."
" If you are a developer/maintainer, please make a greater "
"effort in the future to receive valid responses. For tips on how, "
"come have a chat with us in ##3rd-party-dev-and-esi on the EVE "
"Online Discord. https://www.eveonline.com/discord")
}

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.13 on 2024-05-12 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0022_userprofile_theme'),
]
operations = [
migrations.AlterField(
model_name='userprofile',
name='language',
field=models.CharField(blank=True, choices=[('en', 'English'), ('de', 'German'), ('es', 'Spanish'), ('zh-hans', 'Chinese Simplified'), ('ru', 'Russian'), ('ko', 'Korean'), ('fr', 'French'), ('ja', 'Japanese'), ('it', 'Italian'), ('uk', 'Ukrainian'), ('pl', 'Polish')], default='', max_length=10, verbose_name='Language'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2 on 2024-09-13 09:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0023_alter_userprofile_language'),
]
operations = [
migrations.AlterField(
model_name='userprofile',
name='language',
field=models.CharField(blank=True, choices=[('en', 'English'), ('cs-cz', 'Czech'), ('de', 'German'), ('es', 'Spanish'), ('it-it', 'Italian'), ('ja', 'Japanese'), ('ko-kr', 'Korean'), ('fr-fr', 'French'), ('ru', 'Russian'), ('nl-nl', 'Dutch'), ('pl-pl', 'Polish'), ('uk', 'Ukrainian'), ('zh-hans', 'Simplified Chinese')], default='', max_length=10, verbose_name='Language'),
),
]

View File

@ -1,5 +1,4 @@
import logging
from typing import ClassVar
from django.contrib.auth.models import User, Permission
from django.db import models, transaction
@ -28,7 +27,7 @@ class State(models.Model):
help_text="Factions to whose members this state is available.")
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
objects: ClassVar[StateManager] = StateManager()
objects = StateManager()
class Meta:
ordering = ['-priority']
@ -68,20 +67,17 @@ class UserProfile(models.Model):
"""
Choices for UserProfile.language
"""
# Sorted by Language Code alphabetical order + English at top
ENGLISH = 'en', _('English')
CZECH = 'cs-cz', _("Czech") # Not yet at 50% translated
GERMAN = 'de', _('German')
SPANISH = 'es', _('Spanish')
ITALIAN = 'it-it', _('Italian')
JAPANESE = 'ja', _('Japanese')
KOREAN = 'ko-kr', _('Korean')
FRENCH = 'fr-fr', _('French')
CHINESE = 'zh-hans', _('Chinese Simplified')
RUSSIAN = 'ru', _('Russian')
DUTCH = 'nl-nl', _("Dutch")
POLISH = 'pl-pl', _("Polish")
KOREAN = 'ko', _('Korean')
FRENCH = 'fr', _('French')
JAPANESE = 'ja', _('Japanese')
ITALIAN = 'it', _('Italian')
UKRAINIAN = 'uk', _('Ukrainian')
CHINESE = 'zh-hans', _('Simplified Chinese')
user = models.OneToOneField(
User,
@ -138,10 +134,8 @@ class UserProfile(models.Model):
sender=self.__class__, user=self.user, state=self.state
)
def __str__(self) -> str:
def __str__(self):
return str(self.user)
class CharacterOwnership(models.Model):
class Meta:
default_permissions = ('change', 'delete')
@ -151,7 +145,7 @@ class CharacterOwnership(models.Model):
owner_hash = models.CharField(max_length=28, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
objects: ClassVar[CharacterOwnershipManager] = CharacterOwnershipManager()
objects = CharacterOwnershipManager()
def __str__(self):
return f"{self.user}: {self.character}"

View File

@ -1,11 +1,13 @@
{% load i18n %}
<div id="aa-dashboard-panel-characters" class="col-12 col-xl-8 mb-3">
<div class="card h-100">
<div class="col-12 col-xl-8 align-self-stretch p-2 ps-0">
<div class="card">
<div class="card-body">
{% translate "Characters" as widget_title %}
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
<div>
<div class="d-flex align-items-center">
<h4 class="ms-auto me-auto">
{% translate "Characters" %}
</h4>
</div>
<div class="card-body">
<div style="height: 300px; overflow-y:auto;">
<div class="d-flex">
<a href="{% url 'authentication:add_character' %}" class="btn btn-primary flex-fill m-1" title="{% translate 'Add Character' %}">

View File

@ -1,13 +1,11 @@
{% load i18n %}
<div id="aa-dashboard-panel-membership" class="col-12 col-xl-4 mb-3">
<div class="col-12 col-xl-4 align-self-stretch py-2 ps-2">
<div class="card h-100">
<div class="card-body">
{% translate "Membership" as widget_title %}
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
<div>
<h4 class="card-title text-center">{% translate "Membership" %}</h4>
<div class="card-body">
<div style="height: 300px; overflow-y:auto;">
<h5 class="text-center">{% translate "State:" %} {{ request.user.profile.state }}</h5>
<h6 class="text-center">{% translate "State:" %} {{ request.user.profile.state }}</h6>
<table class="table">
{% for group in groups %}
<tr>

View File

@ -7,7 +7,7 @@
{% translate "Dashboard" %}
{% endblock %}
{% block content %}
<div class="row">
<div class="d-flex justify-content-around align-self-center flex-wrap">
{% for dash in views %}
{{ dash | safe }}
{% endfor %}

View File

@ -1,10 +0,0 @@
{% extends 'allianceauth/base.html' %}
{% block page_title %}Dashboard{% endblock page_title %}
{% block content %}
<div>
<h1>Dashboard Dummy</h1>
</div>
{% endblock %}

View File

@ -1,89 +1,65 @@
{% extends "allianceauth/base-bs5.html" %}
{% load aa_i18n %}
{% load i18n %}
{% block page_title %}
{% translate "Token Management" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Token Management" %}
{% endblock header_nav_brand %}
{% block page_title %}{% translate "Dashboard" %}{% endblock page_title %}
{% block content %}
<h1 class="page-header text-center">{% translate "Token Management" %}</h1>
<div>
<p class="mb-3">
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://developers.eveonline.com/authorized-apps where possible."|urlize %}
</p>
<table class="table w-100" id="table_tokens">
<table class="table table-aa" id="table_tokens" style="width: 100%;">
<thead>
<tr>
<th>{% translate "Scopes" %}</th>
<th class="text-end">{% translate "Actions" %}</th>
<th>{% translate "Character" %}</th>
</tr>
</thead>
<tbody>
{% for t in tokens %}
<tr>
<td style="white-space:initial;">
{% for s in t.scopes.all %}
<span class="badge text-bg-secondary">{{ s.name }}</span>
{% endfor %}
</td>
<td style="white-space:initial;">{% for s in t.scopes.all %}<span class="badge bg-secondary">{{ s.name }}</span>{% endfor %}</td>
<td nowrap class="text-end">
<a href="{% url 'authentication:token_delete' t.id %}" class="btn btn-danger"><i class="fa-solid fa-trash-can"></i></a>
<a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fa-solid fa-rotate"></i></a>
<a href="{% url 'authentication:token_delete' t.id %}" class="btn btn-danger"><i class="fas fa-trash"></i></a>
<a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fas fa-sync-alt"></i></a>
</td>
<td>{{ t.character_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
</div>
{% endblock content %}
{% block extra_javascript %}
{% include "bundles/datatables-js-bs5.html" %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script>
$(document).ready(() => {
let grp = 2;
$('#table_tokens').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
'columnDefs': [{orderable: false, targets: [0, 1]}, {
'visible': false,
'targets': grp
}],
'order': [[grp, 'asc']],
'drawCallback': function (settings) {
const api = this.api();
const rows = api.rows({page: 'current'}).nodes();
let last = null;
api.column(grp, {page: 'current'})
.data()
.each((group, i) => {
if (last !== group) {
$(rows).eq(i).before(`<tr class="h5 table-primary"><td colspan="3">${group}</td></tr>`);
last = group;
}
});
},
'stateSave': true
});
});
</script>
{% endblock extra_javascript %}
{% block extra_css %}
{% include "bundles/datatables-css-bs5.html" %}
{% endblock extra_css %}
{% block extra_script %}
$(document).ready(function(){
let grp = 2;
var table = $('#table_tokens').DataTable({
"columnDefs": [{ orderable: false, targets: [0,1] },{ "visible": false, "targets": grp }],
"order": [[grp, 'asc']],
"drawCallback": function (settings) {
var api = this.api();
var rows = api.rows({ page: 'current' }).nodes();
var last = null;
api.column(grp, { page: 'current' })
.data()
.each(function (group, i) {
if (last !== group) {
$(rows).eq(i).before('<tr class="info"><td colspan="3">' + group + '</td></tr>');
last = group;
}
});
},
"stateSave": true,
});
});
{% endblock extra_script %}

View File

@ -1,25 +1,23 @@
{% load theme_tags %}
{% load static %}
<!DOCTYPE html>
<html lang="en" {% theme_html_tags %}>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- End Required meta tags -->
<meta name="description" content="">
<meta name="author" content="">
<!-- TODO Bundle all the site specific stuff up into its own template for easy overide -->
<meta property="og:title" content="{{ SITE_NAME }}">
<meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
<!-- Meta tags -->
{% include 'allianceauth/opengraph.html' %}
{% include 'allianceauth/icons.html' %}
<!-- Meta tags -->
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
{% theme_css %}
{% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %}
{% include 'bundles/auth-framework-css.html' %}
{% block extra_include %}
{% endblock %}
@ -32,23 +30,25 @@
background-size: cover;
}
.card-login {
background: rgba(48 48 48 / 0.7);
color: rgb(255 255 255);
.panel-transparent {
background: rgba(48 48 48 / 0.7);
color: #ffffff;
padding-bottom: 21px;
}
.panel-body {
}
#lang-select {
width: 40%;
margin-left: auto;
margin-right: auto;
}
{% block extra_style %}
{% endblock %}
</style>
</head>
<body>
<div class="container" style="margin-top:150px;">
{% block content %}

View File

@ -1,15 +1,14 @@
{% load i18n %}
<form class="dropdown-item" action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<select class="form-select" onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_available_languages as LANGUAGES %}
{% for lang_code, lang_name in LANGUAGES %}
<option lang="{{ lang_code }}" value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
{{ lang_code|language_name_local|capfirst }} ({{ lang_code }})
</option>
{% endfor %}
</select>
</form>
<div class="dropdown">
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
{{ language.name_local|capfirst }} ({{ language.code }})
</option>
{% endfor %}
</select>
</form>
</div>

View File

@ -3,41 +3,39 @@
{% load i18n %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="col-md-4 col-md-offset-4">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="card card-login border-secondary p-3">
<div class="card-body">
<div class="text-center">
{% block middle_box_content %}
{% endblock %}
</div>
<div class="panel panel-default panel-transparent">
<div class="panel-body">
<div class="col-md-12">
{% block middle_box_content %}
{% endblock %}
</div>
{% include 'public/lang_select.html' %}
<p class="text-center mt-3">
{% translate "For information on SSO, ESI and security read the CCP Dev Blog" %}<br>
<a class="text-reset" href="https://www.eveonline.com/news/view/introducing-esi" target="_blank" rel="noopener noreferrer">
{% translate "Introducing ESI - A New API For EVE Online" %}
</a>
</p>
<p class="text-center">
<a class="text-reset" href="https://developers.eveonline.com/authorized-apps" target="_blank" rel="noopener noreferrer">
{% translate "Manage ESI Applications" %}
</a>
</p>
</div>
{% include 'public/lang_select.html' %}
<p class="text-center" style="margin-top: 2rem;">
{% translate "For information on SSO, ESI and security read the CCP Dev Blog" %}<br>
<a href="https://www.eveonline.com/article/introducing-esi" target="_blank" rel="noopener noreferrer">
{% translate "Introducing ESI - A New API For Eve Online" %}
</a>
</p>
<p class="text-center">
<a href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
{% translate "Manage ESI Applications" %}
</a>
</p>
</div>
</div>
{% endblock %}
{% block extra_include %}
{% include 'bundles/bootstrap-js-bs5.html' %}
{% include 'bundles/bootstrap-js.html' %}
{% endblock %}

View File

@ -1,31 +1,27 @@
{% extends 'public/base.html' %}
{% load django_bootstrap5 %}
{% load bootstrap %}
{% load i18n %}
{% block page_title %}{% translate "Registration" %}{% endblock %}
{% block extra_include %}
{% include 'bundles/bootstrap-css-bs5.html' %}
{% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %}
{% include 'bundles/bootstrap-js-bs5.html' %}
{% include 'bundles/bootstrap-js.html' %}
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card card-login border-secondary p-3">
<div class="card-body">
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-primary btn-block" type="submit">{% translate "Register" %}</button>
</form>
{% include 'public/lang_select.html' %}
</div>
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default panel-transparent">
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Register" %}</button>
</form>
</div>
</div>
{% include 'public/lang_select.html' %}
</div>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends 'public/middle_box.html' %}
{% load i18n %}
{% block middle_box_content %}
<div class="alert alert-danger">{% translate 'Invalid or expired activation link.' %}</div>
{% endblock %}

View File

@ -1,12 +1,10 @@
import json
import requests_mock
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from allianceauth.authentication.views import task_counts, esi_check
from allianceauth.authentication.views import task_counts
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
MODULE_PATH = "allianceauth.authentication.views"
@ -23,8 +21,6 @@ class TestRunningTasksCount(TestCase):
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
def test_should_return_data(
self, mock_active_tasks_count, mock_queued_tasks_count
@ -39,164 +35,5 @@ class TestRunningTasksCount(TestCase):
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"tasks_running": 2, "tasks_queued": 3}
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
)
def test_su_only(
self, mock_active_tasks_count, mock_queued_tasks_count
):
self.user.is_superuser = False
self.user.save()
self.user.refresh_from_db()
# given
mock_active_tasks_count.return_value = 2
mock_queued_tasks_count.return_value = 3
request = self.factory.get("/")
request.user = self.user
# when
response = task_counts(request)
# then
self.assertEqual(response.status_code, 302)
class TestEsiCheck(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
@requests_mock.Mocker()
def test_401_data_returns_200(
self, m
):
error_json = {
"error": "You have been banned from using ESI. Please contact Technical Support. (support@eveonline.com)"
}
status_code = 401
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": error_json
}
)
@requests_mock.Mocker()
def test_504_data_returns_200(
self, m
):
error_json = {
"error": "Gateway timeout message",
"timeout": 5000
}
status_code = 504
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": error_json
}
)
@requests_mock.Mocker()
def test_420_data_override(
self, m
):
error_json = {
"error": "message from CCP",
}
status_code = 420
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertNotEqual(
jsonresponse_to_dict(response)["data"],
error_json
)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": {
"error": ESI_ERROR_MESSAGE_OVERRIDES.get(status_code)
}
}
)
@requests_mock.Mocker()
def test_200_data_returns_200(
self, m
):
good_json = {
"players": 5,
"server_version": "69420",
"start_time": "2030-01-01T23:59:59Z"
}
status_code = 200
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(good_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": good_json
}
)
def test_su_only(
self,
):
self.user.is_superuser = False
self.user.save()
self.user.refresh_from_db()
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 302)

View File

@ -38,7 +38,5 @@ urlpatterns = [
name='token_refresh'
),
path('dashboard/', views.dashboard, name='dashboard'),
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
path('task-counts/', views.task_counts, name='task_counts'),
path('esi-check/', views.esi_check, name='esi_check'),
]

View File

@ -1,6 +1,6 @@
import logging
from allianceauth.hooks import get_hooks
import requests
from django_registration.backends.activation.views import (
REGISTRATION_SALT, ActivationView as BaseActivationView,
RegistrationView as BaseRegistrationView,
@ -10,7 +10,7 @@ from django_registration.signals import user_registered
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import signing
from django.http import JsonResponse
@ -23,16 +23,14 @@ from esi.decorators import token_required
from esi.models import Token
from allianceauth.eveonline.models import EveCharacter
from allianceauth.hooks import get_hooks
from .constants import ESI_ERROR_MESSAGE_OVERRIDES
from .core.celery_workers import active_tasks_count, queued_tasks_count
from .forms import RegistrationForm
from .models import CharacterOwnership
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
from allianceauth.eveonline.autogroups.models import * # noqa: F401, F403
from allianceauth.eveonline.autogroups.models import *
else:
_has_auto_groups = False
@ -56,7 +54,7 @@ def dashboard_groups(request):
context = {
'groups': groups,
}
return render_to_string('authentication/dashboard_groups.html', context=context, request=request)
return render_to_string('authentication/dashboard.groups.html', context=context, request=request)
def dashboard_characters(request):
@ -68,7 +66,7 @@ def dashboard_characters(request):
context = {
'characters': characters
}
return render_to_string('authentication/dashboard_characters.html', context=context, request=request)
return render_to_string('authentication/dashboard.characters.html', context=context, request=request)
def dashboard_admin(request):
@ -78,13 +76,6 @@ def dashboard_admin(request):
return ""
def dashboard_esi_check(request):
if request.user.is_superuser:
return render_to_string('allianceauth/admin-status/esi_check.html', request=request)
else:
return ""
@login_required
def dashboard(request):
_dash_items = list()
@ -144,30 +135,23 @@ def token_refresh(request, token_id=None):
@login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token):
logger.debug(
f"main_character_change called by user {request.user} for character {token.character_name}")
logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}")
try:
co = CharacterOwnership.objects.get(
character__character_id=token.character_id, user=request.user)
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist:
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
co = CharacterOwnership.objects.create_by_token(token)
else:
messages.error(
request,
_('Cannot change main character to %(char)s: character owned by a different account.') % (
{'char': token.character_name})
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
)
co = None
if co:
request.user.profile.main_character = co.character
request.user.profile.save(update_fields=['main_character'])
messages.success(request, _('Changed main character to %s') % co.character)
logger.info(
'Changed user {user} main character to {char}'.format(
user=request.user, char=co.character
)
)
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character})
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character}))
return redirect("authentication:dashboard")
@ -175,11 +159,9 @@ def main_character_change(request, token):
def add_character(request, token):
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
messages.success(request, _(
'Added %(name)s to your account.' % ({'name': token.character_name})))
messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name})))
else:
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % (
{'name': token.character_name})))
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
return redirect('authentication:dashboard')
@ -218,15 +200,7 @@ def sso_login(request, token):
request.session['registration_uid'] = user.pk
# Go to Step 2
return redirect('registration_register')
# Logging in with an alt is not allowed due to security concerns.
token.delete()
messages.error(
request,
_(
'Unable to authenticate as the selected character. '
'Please log in with the main character associated with this account.'
)
)
messages.error(request, _('Unable to authenticate as the selected character.'))
return redirect(settings.LOGIN_URL)
@ -298,8 +272,7 @@ class RegistrationView(BaseRegistrationView):
return super().dispatch(request, *args, **kwargs)
def register(self, form):
user = User.objects.get(
pk=self.request.session.get('registration_uid'))
user = User.objects.get(pk=self.request.session.get('registration_uid'))
user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request)
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
@ -316,8 +289,7 @@ class RegistrationView(BaseRegistrationView):
def get_email_context(self, activation_key):
context = super().get_email_context(activation_key)
context['url'] = context['site'].domain + \
reverse('registration_activate', args=[activation_key])
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
return context
@ -350,24 +322,20 @@ class ActivationView(BaseActivationView):
def registration_complete(request):
messages.success(request, _(
'Sent confirmation email. Please follow the link to confirm your email address.'))
messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.'))
return redirect('authentication:login')
def activation_complete(request):
messages.success(request, _(
'Confirmed your email address. Please login to continue.'))
messages.success(request, _('Confirmed your email address. Please login to continue.'))
return redirect('authentication:dashboard')
def registration_closed(request):
messages.error(request, _(
'Registration of new accounts is not allowed at this time.'))
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
return redirect('authentication:login')
@user_passes_test(lambda u: u.is_superuser)
def task_counts(request) -> JsonResponse:
"""Return task counts as JSON for an AJAX call."""
data = {
@ -375,31 +343,3 @@ def task_counts(request) -> JsonResponse:
"tasks_queued": queued_tasks_count()
}
return JsonResponse(data)
def check_for_override_esi_error_message(response):
if response.status_code in ESI_ERROR_MESSAGE_OVERRIDES:
return {"error": ESI_ERROR_MESSAGE_OVERRIDES.get(response.status_code)}
else:
return response.json()
@user_passes_test(lambda u: u.is_superuser)
def esi_check(request) -> JsonResponse:
"""Return if ESI ok With error messages and codes as JSON"""
_r = requests.get("https://esi.evetech.net/latest/status/?datasource=tranquility")
data = {
"status": _r.status_code,
"data": check_for_override_esi_error_message(_r)
}
return JsonResponse(data)
@login_required
def dashboard_bs3(request):
"""Render dashboard view with BS3 theme.
This is an internal view used for testing BS3 backward compatibility in AA4 only.
"""
return render(request, 'authentication/dashboard_bs3.html')

View File

@ -12,14 +12,13 @@ class StartProject(BaseStartProject):
parser.add_argument('--python', help='The path to the python executable.')
parser.add_argument('--celery', help='The path to the celery executable.')
parser.add_argument('--gunicorn', help='The path to the gunicorn executable.')
parser.add_argument('--memmon', help='The path to the memmon executable.')
def create_project(parser, options, args):
# Validate args
if len(args) < 2:
parser.error("Please specify a name for your Alliance Auth installation.")
elif len(args) > 4:
elif len(args) > 3:
parser.error("Too many arguments.")
# First find the path to Alliance Auth

View File

@ -1,562 +0,0 @@
"""
Django system checks for Alliance Auth
"""
from typing import List
from django import db
from django.core.checks import CheckMessage, Error, register, Warning
from allianceauth.utils.cache import get_redis_client
from django.utils import timezone
from packaging.version import InvalidVersion, Version as Pep440Version
from celery import current_app
from django.conf import settings
from sqlite3.dbapi2 import sqlite_version_info
"""
A = System Packages
B = Configuration
"""
@register()
def django_settings(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that Django settings are correctly configured
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
errors: List[CheckMessage] = []
# Check for SITE_URL
if hasattr(settings, "SITE_URL"):
# Check if SITE_URL is empty
if settings.SITE_URL == "":
errors.append(
Error(
msg="'SITE_URL' is empty.",
hint="Make sure to set 'SITE_URL' to the URL of your Auth instance. (Without trailing slash)",
id="allianceauth.checks.B011",
)
)
# Check if SITE_URL has a trailing slash
elif settings.SITE_URL[-1] == "/":
errors.append(
Warning(
msg="'SITE_URL' has a trailing slash. This may lead to incorrect links being generated by Auth.",
hint="",
id="allianceauth.checks.B005",
)
)
# SITE_URL not found
else:
errors.append(
Error(
msg="No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.",
hint="",
id="allianceauth.checks.B006",
)
)
# Check for CSRF_TRUSTED_ORIGINS
if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"):
# Check if SITE_URL is not in CSRF_TRUSTED_ORIGINS
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
errors.append(
Warning(
msg="'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.",
hint="",
id="allianceauth.checks.B007",
)
)
# CSRF_TRUSTED_ORIGINS not found
else:
errors.append(
Error(
msg="No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified",
hint="",
id="allianceauth.checks.B008",
)
)
# Check for ESI_USER_CONTACT_EMAIL
if hasattr(settings, "ESI_USER_CONTACT_EMAIL"):
# Check if ESI_USER_CONTACT_EMAIL is empty
if settings.ESI_USER_CONTACT_EMAIL == "":
errors.append(
Error(
msg="'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.",
hint="",
id="allianceauth.checks.B009",
)
)
# ESI_USER_CONTACT_EMAIL not found
else:
errors.append(
Error(
msg="No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.",
hint="",
id="allianceauth.checks.B010",
)
)
return errors
@register()
def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that Redis is a supported version
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
allianceauth_redis_install_link = "https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools"
errors: List[CheckMessage] = []
try:
redis_version = Pep440Version(get_redis_client().info()["redis_version"])
except InvalidVersion:
errors.append(Warning("Unable to confirm Redis Version"))
return errors
if (
redis_version.major == 7
and redis_version.minor == 2
and timezone.now()
> timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc)
):
errors.append(
Error(
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
hint=allianceauth_redis_install_link,
id="allianceauth.checks.A001",
)
)
elif redis_version.major == 7 and redis_version.minor == 0:
errors.append(
Warning(
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
hint=allianceauth_redis_install_link,
id="allianceauth.checks.A002",
)
)
elif redis_version.major == 6 and redis_version.minor == 2:
errors.append(
Warning(
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
hint=allianceauth_redis_install_link,
id="allianceauth.checks.A018",
)
)
elif redis_version.major in [6, 5]:
errors.append(
Error(
msg=f"Redis {redis_version.public} EOL",
hint=allianceauth_redis_install_link,
id="allianceauth.checks.A003",
)
)
return errors
@register()
def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that MySQL is a supported version
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
mysql_quick_guide_link = "https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/"
errors: List[CheckMessage] = []
for connection in db.connections.all():
if connection.vendor == "mysql":
try:
mysql_version = Pep440Version(
".".join(str(i) for i in connection.mysql_version)
)
except InvalidVersion:
errors.append(Warning("Unable to confirm MySQL Version"))
return errors
# MySQL 8
if mysql_version.major == 8:
if mysql_version.minor == 4 and timezone.now() > timezone.datetime(
year=2032, month=4, day=30, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MySQL {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A004",
)
)
elif mysql_version.minor == 3:
errors.append(
Warning(
msg=f"MySQL {mysql_version.public} Non LTS",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A005",
)
)
elif mysql_version.minor == 2:
errors.append(
Warning(
msg=f"MySQL {mysql_version.public} Non LTS",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A006",
)
)
elif mysql_version.minor == 1:
errors.append(
Error(
msg=f"MySQL {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A007",
)
)
elif mysql_version.minor == 0 and timezone.now() > timezone.datetime(
year=2026, month=4, day=30, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MySQL {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A008",
)
)
# MySQL below 8
# This will also catch Mariadb 5.x
elif mysql_version.major < 8:
errors.append(
Error(
msg=f"MySQL or MariaDB {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A009",
)
)
return errors
@register()
def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that MariaDB is a supported version
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
mariadb_download_link = "https://mariadb.org/download/?t=repo-config"
errors: List[CheckMessage] = []
for connection in db.connections.all():
# TODO: Find a way to determine MySQL vs. MariaDB
if connection.vendor == "mysql":
try:
mariadb_version = Pep440Version(
".".join(str(i) for i in connection.mysql_version)
)
except InvalidVersion:
errors.append(Warning("Unable to confirm MariaDB Version"))
return errors
# MariaDB 11
if mariadb_version.major == 11:
if mariadb_version.minor == 4 and timezone.now() > timezone.datetime(
year=2029, month=5, day=19, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A010",
)
)
elif mariadb_version.minor == 2:
errors.append(
Warning(
msg=f"MariaDB {mariadb_version.public} Non LTS",
hint=mariadb_download_link,
id="allianceauth.checks.A018",
)
)
if timezone.now() > timezone.datetime(
year=2024, month=11, day=21, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A011",
)
)
elif mariadb_version.minor == 1:
errors.append(
Warning(
msg=f"MariaDB {mariadb_version.public} Non LTS",
hint=mariadb_download_link,
id="allianceauth.checks.A019",
)
)
if timezone.now() > timezone.datetime(
year=2024, month=8, day=21, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A012",
)
)
# Demote versions down here once EOL
elif mariadb_version.minor in [0, 3]:
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A013",
)
)
# MariaDB 10
elif mariadb_version.major == 10:
if mariadb_version.minor == 11 and timezone.now() > timezone.datetime(
year=2028, month=2, day=10, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A014",
)
)
elif mariadb_version.minor == 6 and timezone.now() > timezone.datetime(
year=2026, month=7, day=6, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A0015",
)
)
elif mariadb_version.minor == 5 and timezone.now() > timezone.datetime(
year=2025, month=6, day=24, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A016",
)
)
# Demote versions down here once EOL
elif mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]:
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A017",
)
)
return errors
@register()
def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that SQLite is a supported version
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
errors: List[CheckMessage] = []
for connection in db.connections.all():
if connection.vendor == "sqlite":
try:
sqlite_version = Pep440Version(
".".join(str(i) for i in sqlite_version_info)
)
except InvalidVersion:
errors.append(Warning("Unable to confirm SQLite Version"))
return errors
if sqlite_version.major == 3 and sqlite_version.minor < 27:
errors.append(
Error(
msg=f"SQLite {sqlite_version.public} Unsupported by Django",
hint="https://pkgs.org/download/sqlite3",
id="allianceauth.checks.A020",
)
)
return errors
@register()
def sql_settings(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that SQL settings are correctly configured
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
errors: List[CheckMessage] = []
for connection in db.connections.all():
if connection.vendor == "mysql":
try:
if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4":
errors.append(
Error(
msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
id="allianceauth.checks.B001",
)
)
except KeyError:
errors.append(
Error(
msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
id="allianceauth.checks.B001",
)
)
# This hasn't actually been set on AA yet
# try:
# if (
# connection.settings_dict["OPTIONS"]["collation"]
# != "utf8mb4_unicode_ci"
# ):
# errors.append(
# Error(
# msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}",
# hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
# id="allianceauth.checks.B001",
# )
# )
# except KeyError:
# errors.append(
# Error(
# msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}",
# hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8",
# id="allianceauth.checks.B001",
# )
# )
# if connection.vendor == "sqlite":
return errors
@register()
def celery_settings(app_configs, **kwargs) -> List[CheckMessage]:
"""
Check that Celery settings are correctly configured
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
errors: List[CheckMessage] = []
try:
if current_app.conf.broker_transport_options != {
"priority_steps": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"queue_order_strategy": "priority",
}:
errors.append(
Error(
msg="Celery Priorities are not set correctly",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
id="allianceauth.checks.B003",
)
)
except KeyError:
errors.append(
Error(
msg="Celery Priorities are not set",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
id="allianceauth.checks.B003",
)
)
try:
if not current_app.conf.broker_connection_retry_on_startup:
errors.append(
Error(
msg="Celery broker_connection_retry_on_startup not set correctly",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
id="allianceauth.checks.B004",
)
)
except KeyError:
errors.append(
Error(
msg="Celery broker_connection_retry_on_startup not set",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280",
id="allianceauth.checks.B004",
)
)
return errors
# IDEAS
# Any other celery things weve manually changed over the years
# I'd be happy to add Community App checks, old versions the owners dont want to support etc.
# Check Default Collation on DB
# Check Charset Collation on all tables

View File

@ -1,8 +1,6 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class CorpUtilsConfig(AppConfig):
name = 'allianceauth.corputils'
label = 'corputils'
verbose_name = _('Corporation Stats')

View File

@ -10,7 +10,7 @@ class CorpStats(MenuItemHook):
MenuItemHook.__init__(
self,
_('Corporation Stats'),
'fa-solid fa-share-nodes',
'fas fa-share-alt fa-fw',
'corputils:view',
navactive=['corputils:']
)

View File

@ -1,6 +1,5 @@
import logging
import os
from typing import ClassVar
from allianceauth.authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden
@ -41,9 +40,9 @@ class CorpStats(models.Model):
verbose_name = "corp stats"
verbose_name_plural = "corp stats"
objects: ClassVar[CorpStatsManager] = CorpStatsManager()
objects = CorpStatsManager()
def __str__(self) -> str:
def __str__(self):
return f"{self.__class__.__name__} for {self.corp}"
def update(self):

View File

@ -1,62 +1,39 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Corporation Member Data" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Corporation Member Data" %}
{% endblock header_nav_brand %}
{% block header_nav_collapse_left %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
{% translate "Corporations" %}
</a>
<ul class="dropdown-menu">
{% for corpstat in available %}
<li>
<a class="dropdown-item" href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">
{{ corpstat.corp.corporation_name }}
</a>
</li>
{% endfor %}
{% if perms.corputils.add_corpstats %}
{% if available.count >= 1 %}
<li>&nbsp;</li>
{% endif %}
<li>
<a class="dropdown-item" href="{% url 'corputils:add' %}">
{% translate "Add corporation" %}
</a>
</li>
{% endif %}
</ul>
</li>
{% endblock %}
{% block header_nav_collapse_right %}
<li class="nav-item">
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
<div class="form-group">
<input
type="text"
class="form-control"
name="search_string"
placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate 'Search all corporations...' %}{% endif %}"
>
</div>
</form>
</li>
{% endblock %}
{% block content %}
<div>
<h1 class="page-header text-center">{% translate "Corporation Member Data" %}</h1>
<nav class="navbar navbar-default">
<div class="container-fluid">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% translate "Corporations" %}<span class="caret"></span></a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
{% for corpstat in available %}
<li>
<a href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">{{ corpstat.corp.corporation_name }}</a>
</li>
{% endfor %}
</ul>
</li>
{% if perms.corputils.add_corpstats %}
<li>
<a href="{% url 'corputils:add' %}">{% translate "Add" %}</a>
</li>
{% endif %}
</ul>
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
<div class="form-group">
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate 'Search all corporations...' %}{% endif %}">
</div>
</form>
</div>
</nav>
{% block member_data %}
{% endblock member_data %}
</div>

View File

@ -1,234 +1,178 @@
{% extends 'corputils/base.html' %}
{% load aa_i18n %}
{% load i18n %}
{% load humanize %}
{% block member_data %}
{% if corpstats %}
<div>
<table class="table text-center">
<table class="table">
<tr>
<td>
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}">
</td>
<td class="text-center col-lg-6{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}">
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}">
</td>
{% if corpstats.corp.alliance %}
<td>
<td class="text-center col-lg-6">
<img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}" alt="{{ corpstats.corp.alliance.alliance_name }}">
</td>
{% endif %}
</tr>
<tr>
<td><p class="h4">{{ corpstats.corp.corporation_name }}</p></td>
<td class="text-center"><h4>{{ corpstats.corp.corporation_name }}</h4></td>
{% if corpstats.corp.alliance %}
<td><p class="h4">{{ corpstats.corp.alliance.alliance_name }}</p></td>
<td class="text-center"><h4>{{ corpstats.corp.alliance.alliance_name }}</h4></td>
{% endif %}
</tr>
</table>
</div>
<div class="card card-default mt-4">
<div class="card-header clearfix" role="tablist">
<ul class="nav nav-pills float-start">
<li class="nav-item" role="presentation">
<a
class="nav-link active"
id="mains"
data-bs-toggle="tab"
href="#tab-mains"
role="tab"
aria-controls="tab-mains"
aria-selected="true"
>
{% translate 'Mains' %} ({{ total_mains }})
<div>
<div class="panel panel-default">
<div class="panel-heading">
<ul class="nav nav-pills pull-left">
<li class="active"><a href="#mains" data-toggle="pill">{% translate 'Mains' %} ({{ total_mains }})</a></li>
<li><a href="#members" data-toggle="pill">{% translate 'Members' %} ({{ corpstats.member_count }})</a></li>
<li><a href="#unregistered" data-toggle="pill">{% translate 'Unregistered' %} ({{ unregistered.count }})</a></li>
</ul>
<div class="pull-right hidden-xs">
{% translate "Last update:" %} {{ corpstats.last_update|naturaltime }}&nbsp;
<a class="btn btn-success" type="button" href="{% url 'corputils:update' corpstats.corp.corporation_id %}" title="Update Now">
<span class="glyphicon glyphicon-refresh"></span>
</a>
</li>
<li class="nav-item" role="presentation">
<a
class="nav-link"
id="members"
data-bs-toggle="tab"
href="#tab-members"
role="tab"
aria-controls="tab-members"
aria-selected="false"
>
{% translate 'Members' %} ({{ corpstats.member_count }})
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
id="unregistered"
data-bs-toggle="tab"
href="#tab-unregistered"
role="tab"
aria-controls="tab-unregistered"
aria-selected="false"
>
{% translate 'Unregistered' %} ({{ unregistered.count }})
</a>
</li>
</ul>
<div class="float-end d-none d-sm-block">
{% translate "Last update:" %} {{ corpstats.last_update|naturaltime }}
<a
class="btn btn-success btn-sm ms-2"
type="button"
href="{% url 'corputils:update' corpstats.corp.corporation_id %}"
title="{% translate 'Update Now' %}"
>
<i class="fa-solid fa-rotate"></i>
</a>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-mains" role="tabpanel" aria-labelledby="tab-mains">
{% if mains %}
<div class="table-responsive">
<table class="table table-hover" id="table-mains">
<thead>
<tr>
<th>{% translate "Main character" %}</th>
<th>{% translate "Registered characters" %}</th>
</tr>
</thead>
<tbody>
{% for id, main in mains.items %}
<div class="panel-body">
<div class="tab-content">
<div class="tab-pane fade in active" id="mains">
{% if mains %}
<div class="table-responsive">
<table class="table table-hover" id="table-mains">
<thead>
<tr>
<td class="text-center" style="vertical-align: middle;">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
<div class="caption">
{{ main.main }}
<th style="height:1em;"><!-- Must have text or height to prevent clipping --></th>
<th></th>
</tr>
</thead>
<tbody>
{% for id, main in mains.items %}
<tr>
<td class="text-center" style="vertical-align:middle">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
<div class="caption text-center">
{{ main.main }}
</div>
</div>
</div>
</td>
<td>
<table class="table table-hover">
{% for alt in main.alts|dictsort:"character_name" %}
{% if forloop.first %}
</td>
<td>
<table class="table table-hover">
{% for alt in main.alts %}
{% if forloop.first %}
<tr>
<th></th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center">{% translate "Corporation" %}</th>
<th class="text-center">{% translate "Alliance" %}</th>
<th class="text-center"></th>
</tr>
{% endif %}
<tr>
<th></th>
<th>{% translate "Character" %}</th>
<th>{% translate "Corporation" %}</th>
<th>{% translate "Alliance" %}</th>
<th></th>
<td class="text-center" style="width:5%">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}">
</div>
</td>
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
<td class="text-center" style="width:5%">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge bg-danger" target="_blank">
{% translate "Killboard" %}
</a>
</td>
</tr>
{% endif %}
<tr>
<td style="width: 5%;">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}">
</div>
</td>
<td style="width: 30%;">{{ alt.character_name }}</td>
<td style="width: 30%;">{{ alt.corporation_name }}</td>
<td style="width: 30%;">{{ alt.alliance_name|default_if_none:"" }}</td>
<td style="width: 5%;">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge text-bg-danger" target="_blank">
{% translate "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="tab-members" role="tabpanel" aria-labelledby="tab-members">
{% if members %}
<div class="table-responsive">
<table class="table table-hover" id="table-members">
<thead>
<tr>
<th></th>
<th>{% translate "Character" %}</th>
<th></th>
<th>{% translate "Main Character" %}</th>
<th>{% translate "Main Corporation" %}</th>
<th>{% translate "Main Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for member in members %}
{% endfor %}
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="members">
{% if members %}
<div class="table-responsive">
<table class="table table-hover" id="table-members">
<thead>
<tr>
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
<td>{{ member }}</td>
<td>
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a>
</td>
<td>{{ member.character_ownership.user.profile.main_character.character_name }}</td>
<td>{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
<td>{{ member.character_ownership.user.profile.main_character.alliance_name|default_if_none:"" }}</td>
<th></th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center"></th>
<th class="text-center">{% translate "Main Character" %}</th>
<th class="text-center">{% translate "Main Corporation" %}</th>
<th class="text-center">{% translate "Main Alliance" %}</th>
</tr>
{% endfor %}
{% for member in unregistered %}
<tr class="table-danger">
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
<td>{{ member.character_name }}</td>
<td>
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a>
</td>
<td></td>
<td></td>
<td></td>
</thead>
<tbody>
{% for member in members %}
<tr>
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
<td class="text-center">{{ member }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.character_name }}</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.alliance_name }}</td>
</tr>
{% endfor %}
{% for member in unregistered %}
<tr class="danger">
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
</td>
<td class="text-center"></td>
<td class="text-center"></td>
<td class="text-center"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="unregistered">
{% if unregistered %}
<div class="table-responsive">
<table class="table table-hover" id="table-unregistered">
<thead>
<tr>
<th></th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center"></th>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="tab-unregistered" role="tabpanel" aria-labelledby="tab-unregistered">
{% if unregistered %}
<div class="table-responsive">
<table class="table table-hover" id="table-unregistered">
<thead>
<tr>
<th></th>
<th>{% translate "Character" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for member in unregistered %}
<tr class="table-danger">
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
<td>{{ member.character_name }}</td>
<td>
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">
{% translate "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</thead>
<tbody>
{% for member in unregistered %}
<tr class="danger">
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">
{% translate "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div>
@ -237,46 +181,40 @@
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script>
$(document).ready(() => {
$('#table-mains').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [
{ "sortable": false, "targets": [1] },
],
"stateSave": true,
"stateDuration": 0
});
$('#table-members').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
"stateSave": true,
"stateDuration": 0
});
$('#table-unregistered').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
"stateSave": true,
"stateDuration": 0
});
});
</script>
{% include 'bundles/datatables-js.html' %}
{% endblock %}
{% block extra_css %}
{% include 'bundles/datatables-css-bs5.html' %}
{% include 'bundles/datatables-css.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function(){
$('#table-mains').DataTable({
"columnDefs": [
{ "sortable": false, "targets": [1] },
],
"stateSave": true,
"stateDuration": 0
});
$('#table-members').DataTable({
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
"stateSave": true,
"stateDuration": 0
});
$('#table-unregistered').DataTable({
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
"stateSave": true,
"stateDuration": 0
});
});
{% endblock %}

View File

@ -1,37 +1,33 @@
{% extends "corputils/base.html" %}
{% load aa_i18n %}
{% load i18n %}
{% block member_data %}
<div class="card card-default">
<div class="card-header clearfix">
<div class="card-title">{% translate "Search Results" %}</div>
<div class="panel panel-default">
<div class="panel-heading clearfix">
<div class="panel-title pull-left">{% translate "Search Results" %}</div>
</div>
<div class="card-body mt-2">
<div class="panel-body">
<table class="table table-hover" id="table-search">
<thead>
<tr>
<th></th>
<th>{% translate "Character" %}</th>
<th>{% translate "Corporation" %}</th>
<th>{% translate "zKillboard" %}</th>
<th>{% translate "Main Character" %}</th>
<th>{% translate "Main Corporation" %}</th>
<th>{% translate "Main Alliance" %}</th>
<th class="text-center"></th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center">{% translate "Corporation" %}</th>
<th class="text-center">{% translate "zKillboard" %}</th>
<th class="text-center">{% translate "Main Character" %}</th>
<th class="text-center">{% translate "Main Corporation" %}</th>
<th class="text-center">{% translate "Main Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr {% if not result.1.registered %}class="danger"{% endif %}>
<td><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
<td>{{ result.1.character_name }}</td>
<td >{{ result.0.corp.corporation_name }}</td>
<td><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
<td>{{ result.1.main_character.character_name }}</td>
<td>{{ result.1.main_character.corporation_name }}</td>
<td>{{ result.1.main_character.alliance_name }}</td>
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
<td class="text-center">{{ result.1.character_name }}</td>
<td class="text-center">{{ result.0.corp.corporation_name }}</td>
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
<td class="text-center">{{ result.1.main_character.character_name }}</td>
<td class="text-center">{{ result.1.main_character.corporation_name }}</td>
<td class="text-center">{{ result.1.main_character.alliance_name }}</td>
</tr>
{% endfor %}
</tbody>
@ -39,23 +35,17 @@
</div>
</div>
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
<script>
$(document).ready(() => {
$('#table-search').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"stateSave": true,
"stateDuration": 0
});
});
</script>
{% include 'bundles/datatables-js.html' %}
{% endblock %}
{% block extra_css %}
{% include 'bundles/datatables-css-bs5.html' %}
{% include 'bundles/datatables-css.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function(){
$('#table-search').DataTable({
"stateSave": true,
"stateDuration": 0
});
});
{% endblock %}

View File

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

View File

@ -1,16 +0,0 @@
"""
Crontab App Config
"""
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class CrontabConfig(AppConfig):
"""
Crontab App Config
"""
name = "allianceauth.crontab"
label = "crontab"
verbose_name = _("Crontab")

View File

@ -1,29 +0,0 @@
# Generated by Django 4.2.16 on 2025-01-20 06:16
import allianceauth.crontab.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='CronOffset',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('minute', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Minute Offset')),
('hour', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Hour Offset')),
('day_of_month', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Day of Month Offset')),
('month_of_year', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Month of Year Offset')),
('day_of_week', models.FloatField(default=allianceauth.crontab.models.random_default, verbose_name='Day of Week Offset')),
],
options={
'verbose_name': 'Cron Offsets',
},
),
]

View File

@ -1,23 +0,0 @@
from random import random
from django.db import models
from django.utils.translation import gettext_lazy as _
from solo.models import SingletonModel
def random_default() -> float:
return random()
class CronOffset(SingletonModel):
minute = models.FloatField(_("Minute Offset"), default=random_default)
hour = models.FloatField(_("Hour Offset"), default=random_default)
day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default)
month_of_year = models.FloatField(_("Month of Year Offset"), default=random_default)
day_of_week = models.FloatField(_("Day of Week Offset"), default=random_default)
def __str__(self) -> str:
return "Cron Offsets"
class Meta:
verbose_name = "Cron Offsets"

View File

@ -1,70 +0,0 @@
from django.core.exceptions import ObjectDoesNotExist
from django_celery_beat.schedulers import (
DatabaseScheduler
)
from django_celery_beat.models import CrontabSchedule
from django.db.utils import OperationalError, ProgrammingError
from celery import schedules
from celery.utils.log import get_logger
from allianceauth.crontab.models import CronOffset
from allianceauth.crontab.utils import offset_cron
logger = get_logger(__name__)
class OffsetDatabaseScheduler(DatabaseScheduler):
"""
Customization of Django Celery Beat, Database Scheduler
Takes the Celery Schedule from local.py and applies our AA Framework Cron Offset, if apply_offset is true
Otherwise it passes it through as normal
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def update_from_dict(self, mapping):
s = {}
try:
cron_offset = CronOffset.get_solo()
except (OperationalError, ProgrammingError, ObjectDoesNotExist) as exc:
# This is just incase we haven't migrated yet or something
logger.warning(
"OffsetDatabaseScheduler: Could not fetch CronOffset (%r). "
"Defering to DatabaseScheduler",
exc
)
return super().update_from_dict(mapping)
for name, entry_fields in mapping.items():
try:
apply_offset = entry_fields.pop("apply_offset", False) # Ensure this pops before django tries to save to ORM
entry = self.Entry.from_entry(name, app=self.app, **entry_fields)
if apply_offset:
entry_fields.update({"apply_offset": apply_offset}) # Reapply this as its gets pulled from config inconsistently.
schedule_obj = entry.schedule
if isinstance(schedule_obj, schedules.crontab):
offset_cs = CrontabSchedule.from_schedule(offset_cron(schedule_obj))
offset_cs, created = CrontabSchedule.objects.get_or_create(
minute=offset_cs.minute,
hour=offset_cs.hour,
day_of_month=offset_cs.day_of_month,
month_of_year=offset_cs.month_of_year,
day_of_week=offset_cs.day_of_week,
timezone=offset_cs.timezone,
)
entry.schedule = offset_cron(schedule_obj) # This gets passed into Celery Beats Memory, important to keep it in sync with the model/DB
entry.model.crontab = offset_cs
entry.model.save()
logger.debug(f"Offset applied for '{name}' due to 'apply_offset' = True.")
if entry.model.enabled:
s[name] = entry
except Exception as e:
logger.exception("Error updating schedule for %s: %r", name, e)
self.schedule.update(s)

View File

@ -1,63 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from allianceauth.crontab.models import CronOffset
class CronOffsetModelTest(TestCase):
def test_cron_offset_is_singleton(self):
"""
Test that CronOffset is indeed a singleton and that
multiple calls to get_solo() return the same instance.
"""
offset1 = CronOffset.get_solo()
offset2 = CronOffset.get_solo()
# They should be the exact same object in memory
self.assertEqual(offset1.pk, offset2.pk)
def test_default_values_random(self):
"""
Test that the default values are set via random_default() when
no explicit value is provided. We'll patch 'random.random' to
produce predictable output.
"""
with patch('allianceauth.crontab.models.random', return_value=0.1234):
# Force creation of a new CronOffset by clearing the existing one
CronOffset.objects.all().delete()
offset = CronOffset.get_solo() # This triggers creation
# All fields should be 0.1234, because we patched random()
self.assertAlmostEqual(offset.minute, 0.1234)
self.assertAlmostEqual(offset.hour, 0.1234)
self.assertAlmostEqual(offset.day_of_month, 0.1234)
self.assertAlmostEqual(offset.month_of_year, 0.1234)
self.assertAlmostEqual(offset.day_of_week, 0.1234)
def test_update_offset_values(self):
"""
Test that we can update the offsets and retrieve them.
"""
offset = CronOffset.get_solo()
offset.minute = 0.5
offset.hour = 0.25
offset.day_of_month = 0.75
offset.month_of_year = 0.99
offset.day_of_week = 0.33
offset.save()
# Retrieve again to ensure changes persist
saved_offset = CronOffset.get_solo()
self.assertEqual(saved_offset.minute, 0.5)
self.assertEqual(saved_offset.hour, 0.25)
self.assertEqual(saved_offset.day_of_month, 0.75)
self.assertEqual(saved_offset.month_of_year, 0.99)
self.assertEqual(saved_offset.day_of_week, 0.33)
def test_str_representation(self):
"""
Verify the __str__ method returns 'Cron Offsets'.
"""
offset = CronOffset.get_solo()
self.assertEqual(str(offset), "Cron Offsets")

View File

@ -1,80 +0,0 @@
# myapp/tests/test_tasks.py
import logging
from unittest.mock import patch
from django.test import TestCase
from django.db import ProgrammingError
from celery.schedules import crontab
from allianceauth.crontab.utils import offset_cron
from allianceauth.crontab.models import CronOffset
logger = logging.getLogger(__name__)
class TestOffsetCron(TestCase):
def test_offset_cron_normal(self):
"""
Test that offset_cron modifies the minute/hour fields
based on the CronOffset values when everything is normal.
"""
# We'll create a mock CronOffset instance
mock_offset = CronOffset(minute=0.5, hour=0.5)
# Our initial crontab schedule
original_schedule = crontab(
minute=[0, 5, 55],
hour=[0, 3, 23],
day_of_month='*',
month_of_year='*',
day_of_week='*'
)
# Patch CronOffset.get_solo to return our mock offset
with patch('allianceauth.crontab.models.CronOffset.get_solo', return_value=mock_offset):
new_schedule = offset_cron(original_schedule)
# Check the new minute/hour
# minute 0 -> 0 + round(60 * 0.5) = 30 % 60 = 30
# minute 5 -> 5 + 30 = 35 % 60 = 35
# minute 55 -> 55 + 30 = 85 % 60 = 25 --> sorted => 25,30,35
self.assertEqual(new_schedule._orig_minute, '25,30,35')
# hour 0 -> 0 + round(24 * 0.5) = 12 % 24 = 12
# hour 3 -> 3 + 12 = 15 % 24 = 15
# hour 23 -> 23 + 12 = 35 % 24 = 11 --> sorted => 11,12,15
self.assertEqual(new_schedule._orig_hour, '11,12,15')
# Check that other fields are unchanged
self.assertEqual(new_schedule._orig_day_of_month, '*')
self.assertEqual(new_schedule._orig_month_of_year, '*')
self.assertEqual(new_schedule._orig_day_of_week, '*')
def test_offset_cron_programming_error(self):
"""
Test that if a ProgrammingError is raised (e.g. before migrations),
offset_cron just returns the original schedule.
"""
original_schedule = crontab(minute=[0, 15, 30], hour=[1, 2, 3])
# Force get_solo to raise ProgrammingError
with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=ProgrammingError()):
new_schedule = offset_cron(original_schedule)
# Should return the original schedule unchanged
self.assertEqual(new_schedule, original_schedule)
def test_offset_cron_unexpected_exception(self):
"""
Test that if any other exception is raised, offset_cron
also returns the original schedule, and logs the error.
"""
original_schedule = crontab(minute='0', hour='0')
# Force get_solo to raise a generic Exception
with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=Exception("Something bad")):
new_schedule = offset_cron(original_schedule)
# Should return the original schedule unchanged
self.assertEqual(new_schedule, original_schedule)

View File

@ -1,50 +0,0 @@
from celery.schedules import crontab
import logging
from allianceauth.crontab.models import CronOffset
from django.db import ProgrammingError
logger = logging.getLogger(__name__)
def offset_cron(schedule: crontab) -> crontab:
"""Take a crontab and apply a series of precalculated offsets to spread out tasks execution on remote resources
Args:
schedule (crontab): celery.schedules.crontab()
Returns:
crontab: A crontab with offsetted Minute and Hour fields
"""
try:
cron_offset = CronOffset.get_solo()
# Stops this shit from happening 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23
# It is only cosmetic, but still annoying
if schedule._orig_minute == '*':
new_minute = '*'
else:
new_minute = [(m + (round(60 * cron_offset.minute))) % 60 for m in schedule.minute]
if schedule._orig_hour == '*':
new_hour = '*'
else:
new_hour = [(m + (round(24 * cron_offset.hour))) % 24 for m in schedule.hour]
return crontab(
minute=",".join(str(m) for m in sorted(new_minute)),
hour=",".join(str(h) for h in sorted(new_hour)),
day_of_month=schedule._orig_day_of_month,
month_of_year=schedule._orig_month_of_year,
day_of_week=schedule._orig_day_of_week)
except ProgrammingError as e:
# If this is called before migrations are run hand back the default schedule
# These offsets are stored in a Singleton Model,
logger.error(e)
return schedule
except Exception as e:
# We absolutely cant fail to hand back a schedule
logger.error(e)
return schedule

View File

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

View File

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

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

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

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

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

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

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

View File

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

View File

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

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

@ -1,8 +1,6 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class EveonlineConfig(AppConfig):
name = 'allianceauth.eveonline'
label = 'eveonline'
verbose_name = _('EVE Online')

View File

@ -1,11 +1,9 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class EveAutogroupsConfig(AppConfig):
name = 'allianceauth.eveonline.autogroups'
label = 'eve_autogroups'
verbose_name = _('EVE Online Autogroups')
def ready(self):
import allianceauth.eveonline.autogroups.signals

View File

@ -1,5 +1,4 @@
import logging
from typing import ClassVar
from django.db import models, transaction
from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist
@ -39,13 +38,13 @@ class AutogroupsConfigManager(models.Manager):
"""
if state is None:
state = user.profile.state
for config in self.filter(states=state):
# grant user new groups for their state
config.update_group_membership_for_user(user)
for config in self.exclude(states=state):
# ensure user does not have groups from previous state
config.remove_user_from_alliance_groups(user)
config.remove_user_from_corp_groups(user)
for config in self.filter(states=state):
# grant user new groups for their state
config.update_group_membership_for_user(user)
class AutogroupsConfig(models.Model):
@ -79,7 +78,7 @@ class AutogroupsConfig(models.Model):
max_length=10, default='', blank=True,
help_text='Any spaces in the group name will be replaced with this.')
objects: ClassVar[AutogroupsConfigManager] = AutogroupsConfigManager()
objects = AutogroupsConfigManager()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -1,4 +1,3 @@
from allianceauth.eveonline.models import EveCorporationInfo
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
@ -74,51 +73,3 @@ class AutogroupsConfigManagerTestCase(TestCase):
AutogroupsConfig.objects.update_groups_for_user(member)
self.assertTrue(update_groups.called)
def test_update_group_membership_corp_in_two_configs(self):
# given
member = AuthUtils.create_member('test member')
AuthUtils.add_main_character_2(
member,
character_id='1234',
name='test character',
corp_id='2345',
corp_name='corp name',
corp_ticker='TIKK',
)
corp = EveCorporationInfo.objects.create(
corporation_id='2345',
corporation_name='corp name',
corporation_ticker='TIKK',
member_count=10,
)
member_state = AuthUtils.get_member_state()
member_config = AutogroupsConfig.objects.create(corp_groups=True)
member_config.states.add(member_state)
blue_state = AuthUtils.get_blue_state()
blue_state.member_corporations.add(corp)
blue_config = AutogroupsConfig.objects.create(corp_groups=True)
blue_config.states.add(blue_state)
member.profile.state = blue_state
member.profile.save()
AutogroupsConfig.objects.update_groups_for_user(member)
# Checks before test that the role is correctly applied
group = blue_config.get_corp_group(corp)
self.assertIn(group, member.groups.all())
# when
blue_state.member_corporations.remove(corp)
member_state.member_corporations.add(corp)
member.profile.state = member_state
member.profile.save()
# then
AutogroupsConfig.objects.update_groups_for_user(member)
group = member_config.get_corp_group(corp)
self.assertIn(group, member.groups.all())

View File

@ -10,7 +10,7 @@ from . import (
)
_BASE_URL = 'https://evemaps.dotlan.net'
_BASE_URL = 'http://evemaps.dotlan.net'
def _build_url(category: str, name: str) -> str:

View File

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

View File

@ -14,20 +14,10 @@ class EveCharacterProviderManager:
class EveCharacterManager(models.Manager):
provider = EveCharacterProviderManager()
def exclude_biomassed(self):
"""
Get a queryset of EveCharacter objects, excluding the "Doomheim" corporation (1000001).
:return:
:rtype:
"""
return self.exclude(corporation_id=1000001)
def create_character(self, character_id) -> models.Model:
def create_character(self, character_id):
return self.create_character_obj(self.provider.get_character(character_id))
def create_character_obj(self, character: providers.Character) -> models.Model:
def create_character_obj(self, character: providers.Character):
return self.create(
character_id=character.id,
character_name=character.name,

View File

@ -1,5 +1,5 @@
import logging
from typing import ClassVar, Union
from typing import Union
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
@ -75,8 +75,8 @@ class EveAllianceInfo(models.Model):
alliance_ticker = models.CharField(max_length=254)
executor_corp_id = models.PositiveIntegerField()
objects: ClassVar[EveAllianceManager] = EveAllianceManager()
provider: ClassVar[EveAllianceProviderManager] = EveAllianceProviderManager()
objects = EveAllianceManager()
provider = EveAllianceProviderManager()
class Meta:
indexes = [models.Index(fields=['executor_corp_id',])]
@ -147,7 +147,7 @@ class EveCorporationInfo(models.Model):
EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL
)
objects: ClassVar[EveCorporationManager] = EveCorporationManager()
objects = EveCorporationManager()
provider = EveCorporationProviderManager()
class Meta:
@ -214,7 +214,7 @@ class EveCharacter(models.Model):
faction_id = models.PositiveIntegerField(blank=True, null=True, default=None)
faction_name = models.CharField(max_length=254, blank=True, null=True, default='')
objects: ClassVar[EveCharacterManager] = EveCharacterManager()
objects = EveCharacterManager()
provider = EveCharacterProviderManager()
class Meta:

View File

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

View File

@ -14,13 +14,15 @@ Needs to be called with a context containing three objects:
{% block page_title %}Evelinks Examples{% endblock page_title %}
{% block content %}
<div>
{% include "framework/header/page-header.html" with title="Evelinks templatetags examples" %}
<div class="col-lg-12">
<h1 class="page-header text-center">Evelinks templatetags examples</h1>
<div class="col-lg-12 container">
<h2>profile URLs</h2>
<div class="rows">
<div class="col-md-4">
<h3>evewho</h3>
<p><a href="{{ my_character|evewho_character_url }}">character from character object</a></p>
@ -55,6 +57,7 @@ Needs to be called with a context containing three objects:
<h2>image URLs</h2>
<div class="rows">
<div class="col-md-4">
<p>character from ID: <img src="{{ my_character.character_id|character_portrait_url:128 }}"></p>
<p>character from character object: <img src="{{ my_character|character_portrait_url:128 }}"></p>
@ -74,4 +77,5 @@ Needs to be called with a context containing three objects:
</div>
</div>
</div>
{% endblock content %}

View File

@ -723,5 +723,5 @@ class TestEveSwaggerProvider(TestCase):
my_client = my_provider.client
operation = my_client.Universe.get_universe_factions()
self.assertEqual(
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 dummy@example.net'
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0'
)

View File

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

View File

@ -1,8 +1,6 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class FatConfig(AppConfig):
name = 'allianceauth.fleetactivitytracking'
label = 'fleetactivitytracking'
verbose_name = _('Fleet Activity Tracking')

View File

@ -7,7 +7,7 @@ from allianceauth.services.hooks import UrlHook
@hooks.register('menu_item_hook')
def register_menu():
return MenuItemHook(_('Fleet Activity Tracking'), 'fa-solid fa-users', 'fatlink:view',
return MenuItemHook(_('Fleet Activity Tracking'), 'fas fa-users fa-fw', 'fatlink:view',
navactive=['fatlink:'])

View File

@ -4,4 +4,4 @@ from django.utils.translation import gettext_lazy as _
class FatlinkForm(forms.Form):
fleet = forms.CharField(label=_("Fleet Name"), max_length=50)
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, max_value=2147483647, help_text=_('Duration of the fat-link in minutes'))
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, max_value=2147483647, help_text=_('minutes'))

View File

@ -1,26 +0,0 @@
"""
Migration to AA Framework API method
"""
from django.conf import settings
from django.db import migrations, models
import allianceauth.framework.api.user
class Migration(migrations.Migration):
dependencies = [
("fleetactivitytracking", "0006_auto_20180803_0430"),
]
operations = [
migrations.AlterField(
model_name="fatlink",
name="creator",
field=models.ForeignKey(
on_delete=models.SET(allianceauth.framework.api.user.get_sentinel_user),
to=settings.AUTH_USER_MODEL
),
),
]

View File

@ -3,7 +3,10 @@ from django.db import models
from django.utils import timezone
from allianceauth.eveonline.models import EveCharacter
from allianceauth.framework.api.user import get_sentinel_user
def get_sentinel_user():
return User.objects.get_or_create(username='deleted')[0]
class Fatlink(models.Model):

View File

@ -1,43 +1,23 @@
{% extends 'allianceauth/base-bs5.html' %}
{% load i18n %}
{% block page_title %}
{% translate "Fleet Participation" %}
{% endblock %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block content %}
<div>
{% translate "Character not found!" as page_header %}
{% include "framework/header/page-header.html" with title=page_header %}
<div class="col-lg-12 container">
<div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Character not found!" %}</h1>
<div class="col-lg-12 container" id="example">
<div class="row">
<div class="col-lg-12">
<div class="card card-default">
<div class="card-header">
<div class="card-title mb-0">
{{ character_name }}
</div>
</div>
<div class="card-body">
<div class="panel panel-default">
<div class="panel-heading">{{ character_name }}</div>
<div class="panel-body">
<div class="col-lg-2 col-sm-2">
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}" alt="{{ character_name }}">
</div>
<div class="col-lg-10 col-sm-2">
<div class="alert alert-danger" role="alert">
{% translate "Character not registered!" %}
</div>
{% translate "This character is not associated with an auth account." %}
<a href="{% url 'authentication:add_character' %}">{% translate "Add it here" %}</a>
{% translate "before attempting to click fleet attendance links." %}
<div class="alert alert-danger" role="alert">{% translate "Character not registered!" %}</div>
{% translate "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% translate "Add it here" %}</a> {% translate "before attempting to click fleet attendance links." %}
</div>
</div>
</div>

View File

@ -1,53 +0,0 @@
{% extends "allianceauth/base-bs5.html" %}
{% load django_bootstrap5 %}
{% load i18n %}
{% block page_title %}
{% translate "Create Fatlink" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block content %}
<div>
{% translate "Create Fatlink" as page_header %}
{% include "framework/header/page-header.html" with title=page_header %}
<div>
{% if badrequest %}
<div class="alert alert-danger" role="alert">{% translate "Bad request!" %}</div>
{% endif %}
{% for message in errormessages %}
<div class="alert alert-danger" role="alert">{{ message }}</div>
{% endfor %}
<div class="card card-primary border-0">
<div class="card-header">
<div class="card-title mb-0">
{% translate "Fatlink details" %}
</div>
</div>
<div class="card-body">
<div class="row justify-content-center">
<div class="col-md-6">
<form role="form" action="" method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group mt-3 clearfix">
{% translate "Create fatlink" as button_text %}
{% bootstrap_button button_class="btn btn-primary" content=button_text name="submit_fat" id="submit_fat" %}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,31 @@
{% extends "allianceauth/base-bs5.html" %}
{% load bootstrap %}
{% load i18n %}
{% block page_title %}
{% translate "Create Fatlink" %}
{% endblock page_title %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Create Fleet Operation" %}</h1>
<div class="container-fluid">
{% if badrequest %}
<div class="alert alert-danger" role="alert">{% translate "Bad request!" %}</div>
{% endif %}
{% for message in errormessages %}<div class="alert alert-danger" role="alert">{{ message }}</div>{% endfor %}
<div class="col-md-4 offset-md-4">
<div class="row">
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br>
<button class="btn btn-lg btn-primary btn-block"
type="submit"
name="submit_fat">
{% translate "Create fatlink" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -1,61 +1,45 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Fatlink view" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
{% block content %}
<div>
<h1 class="page-header text-center mb-3">
{% translate "Edit fatlink" %} "{{ fatlink }}"
</h1>
<div class="text-end mb-3">
<form>
<button type="submit" onclick="return confirm('{% translate "Are you sure?" %}')" class="btn btn-danger" name="deletefat" value="True">
{% translate "Delete fat" %}
</button>
</form>
</div>
<div class="card card-default">
<div class="card-header">
<div class="card-title mb-0">{% translate "Registered characters" %}</div>
<div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Edit fatlink" %} "{{ fatlink }}"
<div class="text-end">
<form>
<button type="submit" onclick="return confirm('Are you sure?')" class="btn btn-danger" name="deletefat" value="True">
{% translate "Delete fat" %}
</button>
</form>
</div>
<div class="card-body">
</h1>
<div class="panel panel-default">
<div class="panel-heading">{% translate "Registered characters" %}</div>
<div class="panel-body">
<table class="table table-responsive table-hover">
<tr>
<th class="text-center">{% translate "User" %}</th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center">{% translate "System" %}</th>
<th class="text-center">{% translate "Ship" %}</th>
<th class="text-center">{% translate "EVE time" %}</th>
<th class="text-center">{% translate "Eve Time" %}</th>
<th></th>
</tr>
{% for fat in registered_fats %}
<tr>
<td class="text-center">{{ fat.user }}</td>
<td class="text-center">{{ fat.character.character_name }}</td>
<td class="text-center">
{% if fat.station != "No Station" %}
{% translate "Docked in" %}
{% endif %}
{{ fat.system }}
</td>
{% if fat.station != "No Station" %}
<td class="text-center">{% blocktranslate %}Docked in {% endblocktranslate %}{{ fat.system }}</td>
{% else %}
<td class="text-center">{{ fat.system }}</td>
{% endif %}
<td class="text-center">{{ fat.shiptype }}</td>
<td class="text-center">{{ fat.fatlink.fatdatetime }}</td>
<td class="text-center">
<form>
<button type="submit" class="btn btn-warning" name="removechar" value="{{ fat.character.character_id }}">
<i class="fa-solid fa-trash-can fa-fw"></i>
<span class="glyphicon glyphicon-remove"></span>
</button>
</form>
</td>

View File

@ -1,104 +1,71 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Personal fatlink statistics" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
{% block content %}
<div>
<h1 class="page-header text-center mb-3">
{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
{% if char_id %}
<div class="text-end">
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% translate "Previous month" %}</a>
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% translate "Next month" %}</a>
</div>
{% endif %}
</h1>
{% if char_id %}
<div class="text-end mb-3">
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">
{% translate "Previous month" %}
</a>
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">
{% translate "Next month" %}
</a>
</div>
{% endif %}
<div class="card card-default mb-3">
<div class="card-header">
<div class="card-title mb-0">
{% blocktranslate count links=n_fats trimmed %}
{{ user }} has collected one link this month.
{% plural %}
{{ user }} has collected {{ links }} links this month.
{% endblocktranslate %}
</div>
</div>
<div class="card-body">
<table class="table table-responsive">
<h2>
{% blocktranslate count links=n_fats trimmed %}
{{ user }} has collected one link this month.
{% plural %}
{{ user }} has collected {{ links }} links this month.
{% endblocktranslate %}
</h2>
<table class="table table-responsive">
<tr>
<th class="col-md-2 text-center">{% translate "Ship" %}</th>
<th class="col-md-2 text-center">{% translate "Times used" %}</th>
</tr>
{% for ship, n_fats in shipStats %}
<tr>
<td class="text-center">{{ ship }}</td>
<td class="text-center">{{ n_fats }}</td>
</tr>
{% endfor %}
</table>
{% if created_fats %}
<h2>
{% blocktranslate count links=n_created_fats trimmed %}
{{ user }} has created one link this month.
{% plural %}
{{ user }} has created {{ links }} links this month.
{% endblocktranslate %}
</h2>
{% if created_fats %}
<table class="table">
<tr>
<th class="col-md-2 text-center">{% translate "Ship" %}</th>
<th class="col-md-2 text-center">{% translate "Times used" %}</th>
<th class="text-center">{% translate "Fleet" %}</th>
<th class="text-center">{% translate "Creator" %}</th>
<th class="text-center">{% translate "Eve Time" %}</th>
<th class="text-center">{% translate "Duration" %}</th>
<th class="text-center">{% translate "Edit" %}</th>
</tr>
{% for link in created_fats %}
<tr>
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a></td>
<td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td>
<td class="text-center">
<a href="{% url 'fatlink:modify' link.hash %}">
<button type="button" class="btn btn-info"><span
class="glyphicon glyphicon-edit"></span></button>
</a>
</td>
</tr>
{% endfor %}
{% for ship, n_fats in shipStats %}
<tr>
<td class="text-center">{{ ship }}</td>
<td class="text-center">{{ n_fats }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% if created_fats %}
<div class="card card-default">
<div class="card-header">
<div class="card-title mb-0">
{% blocktranslate count links=n_created_fats trimmed %}
{{ user }} has created one link this month.
{% plural %}
{{ user }} has created {{ links }} links this month.
{% endblocktranslate %}
</div>
</div>
<div class="card-body">
<table class="table">
<tr>
<th class="text-center">{% translate "Fleet" %}</th>
<th class="text-center">{% translate "Creator" %}</th>
<th class="text-center">{% translate "EVE time" %}</th>
<th class="text-center">{% translate "Duration" %}</th>
<th class="text-center">{% translate "Edit" %}</th>
</tr>
{% for link in created_fats %}
<tr>
<td class="text-center">
<a href="{% url 'fatlink:click' link.hash %}" class="badge text-bg-primary">
{{ link.fleet }}
</a>
</td>
<td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td>
<td class="text-center">
<a href="{% url 'fatlink:modify' link.hash %}">
<button type="button" class="btn btn-info">
<i class="fa-solid fa-pen-to-square fa-fw"></i>
</button>
</a>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
{% endif %}
</div>
{% endblock content %}

View File

@ -1,36 +1,25 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Personal fatlink statistics" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block content %}
<div>
<h1 class="page-header text-center mb-3">
<div class="col-lg-12">
<h1 class="page-header text-center">
{% blocktranslate %}Participation data statistics for {{ year }}{% endblocktranslate %}
<div class="text-end">
<a href="{% url "fatlink:personal_statistics_year" previous_year %}" class="btn btn-info"><i class="fa-solid fa-chevron-left"></i> {% translate "Previous year" %}</a>
{% if next_year %}
<a href="{% url "fatlink:personal_statistics_year" next_year %}" class="btn btn-info">{% translate "Next year" %} <i class="fa-solid fa-chevron-right"></i></a>
{% endif %}
</div>
</h1>
<div class="text-end mb-3">
<a href="{% url "fatlink:personal_statistics_year" previous_year %}" class="btn btn-info"><i class="fa-solid fa-chevron-left"></i> {% translate "Previous year" %}</a>
{% if next_year %}
<a href="{% url "fatlink:personal_statistics_year" next_year %}" class="btn btn-info">{% translate "Next year" %} <i class="fa-solid fa-chevron-right"></i></a>
{% endif %}
</div>
<div class="col-lg-2 offset-lg-5">
<table class="table table-responsive">
<tr>
<th scope="col" class="col-md-2 text-center">{% translate "Month" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
</tr>
{% for monthnr, month, n_fats in monthlystats %}
<tr>
<td class="text-center">

View File

@ -1,63 +1,50 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Fatlink Corp Statistics" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block content %}
<div>
<h1 class="page-header text-center mb-3">
<div class="col-lg-12">
<h1 class="page-header text-center">
{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
</h1>
<div class="text-end mb-3">
<a href="{% url "fatlink:statistics_corp_month" corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
{% if next_month %}
<a href="{% url "fatlink:statistics_corp_month" corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
{% endif %}
</div>
{% if fatStats %}
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th scope="col" class="col-md-1"></th>
<th scope="col" class="col-md-2 text-center">{% translate "Main Character" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Characters" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
<th scope="col" class="col-md-2 text-center">
{% translate "Average fats" %}
<i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
</th>
</tr>
{% for memberStat in fatStats %}
<tr>
<td>
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}">
</td>
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
<td class="text-center">{{ memberStat.n_chars }}</td>
<td class="text-center">{{ memberStat.n_fats }}</td>
<td class="text-center">{{ memberStat.avg_fat }}</td>
</tr>
{% endfor %}
</table>
<div class="text-end">
<a href="{% url "fatlink:statistics_corp_month" corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
{% if next_month %}
<a href="{% url "fatlink:statistics_corp_month" corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
{% endif %}
</div>
</h1>
{% if fatStats %}
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th scope="col" class="col-md-1"></th>
<th scope="col" class="col-md-2 text-center">{% translate "Main Character" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Characters" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
<th scope="col" class="col-md-2 text-center">
{% translate "Average fats" %}
<i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
</th>
</tr>
{% for memberStat in fatStats %}
<tr>
<td>
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}">
</td>
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
<td class="text-center">{{ memberStat.n_chars }}</td>
<td class="text-center">{{ memberStat.n_fats }}</td>
<td class="text-center">{{ memberStat.avg_fat }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
{% endblock content %}
{% block extra_javascript %}
<script>
$(document).ready(() => {
$("[rel=tooltip]").tooltip();
});
</script>
{% endblock extra_javascript %}
{% block extra_script %}
$(document).ready(function () {
$("[rel=tooltip]").tooltip();
});
{% endblock extra_script %}

View File

@ -1,67 +1,54 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Fatlink Statistics" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block content %}
<div>
<h1 class="page-header text-center mb-3">
<div class="col-lg-12">
<h1 class="page-header text-center">
{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
</h1>
<div class="text-end mb-3">
<a href="{% url "fatlink:statistics_month" previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
{% if next_month %}
<a href="{% url 'fatlink:statistics_month' next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
{% endif %}
</div>
{% if fatStats %}
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th scope="col" class="col-md-1"></th>
<th scope="col" class="col-md-2 text-center">{% translate "Ticker" %}</th>
<th scope="col" class="col-md-5 text-center">{% translate "Corp" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Members" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
<th scope="col" class="col-md-2 text-center">
{% translate "Average fats" %}
<i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
</th>
</tr>
{% for corpStat in fatStats %}
<tr>
<td>
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}">
</td>
<td class="text-center">
<a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a>
</td>
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
<td class="text-center">{{ corpStat.corp.member_count }}</td>
<td class="text-center">{{ corpStat.n_fats }}</td>
<td class="text-center">{{ corpStat.avg_fat }}</td>
</tr>
{% endfor %}
</table>
<div class="text-end">
<a href="{% url "fatlink:statistics_month" previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
{% if next_month %}
<a href="{% url 'fatlink:statistics_month' next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
{% endif %}
</div>
</h1>
{% if fatStats %}
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th scope="col" class="col-md-1"></th>
<th scope="col" class="col-md-2 text-center">{% translate "Ticker" %}</th>
<th scope="col" class="col-md-5 text-center">{% translate "Corp" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Members" %}</th>
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
<th scope="col" class="col-md-2 text-center">
{% translate "Average fats" %}
<i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
</th>
</tr>
{% for corpStat in fatStats %}
<tr>
<td>
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}">
</td>
<td class="text-center">
<a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a>
</td>
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
<td class="text-center">{{ corpStat.corp.member_count }}</td>
<td class="text-center">{{ corpStat.n_fats }}</td>
<td class="text-center">{{ corpStat.avg_fat }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
{% endblock content %}
{% block extra_javascript %}
<script>
$(document).ready(() => {
$("[rel=tooltip]").tooltip();
});
</script>
{% endblock extra_javascript %}
{% block extra_script %}
$(document).ready(function () {
$("[rel=tooltip]").tooltip();
});
{% endblock extra_script %}

View File

@ -1,20 +1,11 @@
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% block page_title %}
{% translate "Fatlink view" %}
{% endblock page_title %}
{% block header_nav_brand %}
{% translate "Fleet Activity Tracking" %}
{% endblock header_nav_brand %}
{% block content %}
<div>
{% translate "Participation data" as page_header %}
{% include "framework/header/page-header.html" with title=page_header %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Participation data" %}</h1>
<div class="table-responsive">
<table class="table table-striped">
<tr>
@ -24,15 +15,11 @@
</h4>
</th>
<th class="col-md-2 align-self-end">
<a href="{% url 'fatlink:personal_statistics' %}" class="btn btn-info">
<i class="fa-solid fa-circle-info fa-fw"></i>
{% translate "Personal statistics" %}
</a>
<a href="{% url 'fatlink:personal_statistics' %}" class="btn btn-info"><i class="fa-solid fa-circle-info fa-fw"></i>{% translate "Personal statistics" %}</a>
</th>
</tr>
</table>
</div>
{% if fats %}
<div class="table-responsive">
<table class="table table-striped">
@ -41,9 +28,8 @@
<th scope="col" class="text-center">{% translate "Character" %}</th>
<th scope="col" class="text-center">{% translate "System" %}</th>
<th scope="col" class="text-center">{% translate "Ship" %}</th>
<th scope="col" class="text-center">{% translate "EVE time" %}</th>
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
</tr>
{% for fat in fats %}
<tr>
<td class="text-center">{{ fat.fatlink.fleet }}</td>
@ -62,7 +48,6 @@
{% else %}
<div class="alert alert-warning text-center">{% translate "No fleet activity on record." %}</div>
{% endif %}
{% if perms.auth.fleetactivitytracking %}
<div class="table-responsive">
<table class="table table-striped">
@ -81,7 +66,6 @@
</tr>
</table>
</div>
{% if fatlinks %}
<div class="table-responsive">
<table class="table table-striped">
@ -89,15 +73,14 @@
<th scope="col" class="text-center">{% translate "Name" %}</th>
<th scope="col" class="text-center">{% translate "Creator" %}</th>
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
<th scope="col" class="text-center">{% translate "EVE time" %}</th>
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
<th scope="col" class="text-center">{% translate "Duration" %}</th>
<th scope="col" class="text-center">{% translate "Edit" %}</th>
</tr>
{% for link in fatlinks %}
<tr>
<td class="text-center">
<a href="{% url 'fatlink:click' link.hash %}" class="badge text-bg-primary">{{ link.fleet }}</a>
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a>
</td>
<td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fleet }}</td>

View File

@ -352,11 +352,11 @@ def create_fatlink_view(request):
for errorname, message in e.message_dict.items():
messages.append(message[0].decode())
context = {'form': form, 'errormessages': messages}
return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context)
return render(request, 'fleetactivitytracking/fatlinkformatter.html', context=context)
else:
form = FatlinkForm()
context = {'form': form, 'badrequest': True}
return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context)
return render(request, 'fleetactivitytracking/fatlinkformatter.html', context=context)
return redirect('fatlink:view')
else:
@ -365,7 +365,7 @@ def create_fatlink_view(request):
context = {'form': form}
return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context)
return render(request, 'fleetactivitytracking/fatlinkformatter.html', context=context)
@login_required
@ -377,12 +377,12 @@ def modify_fatlink_view(request, fat_hash=None):
if request.GET.get('removechar', None):
character_id = request.GET.get('removechar')
character = EveCharacter.objects.get(character_id=character_id)
logger.debug(f"Removing character {character.character_name} from fleetactivitytracking {fatlink}")
logger.debug(f"Removing character {character.character_name} from fleetactivitytracking {fatlink}")
Fat.objects.filter(fatlink=fatlink).filter(character=character).delete()
if request.GET.get('deletefat', None):
logger.debug("Removing fleetactivitytracking %s" % fatlink)
logger.debug("Removing fleetactivitytracking %s" % fatlink)
fatlink.delete()
return redirect('fatlink:view')

View File

@ -1,3 +0,0 @@
"""
Alliance Auth Framework
"""

View File

@ -1,57 +0,0 @@
"""
Alliance Auth Evecharacter API
"""
from typing import Optional
from django.contrib.auth.models import User
from allianceauth.authentication.models import CharacterOwnership
from allianceauth.eveonline.models import EveCharacter
from allianceauth.framework.api.user import get_sentinel_user
def get_main_character_from_evecharacter(
character: EveCharacter,
) -> Optional[EveCharacter]:
"""
Get the main character for a given EveCharacter or None when no main character is set
:param character:
:type character:
:return:
:rtype:
"""
try:
userprofile = character.character_ownership.user.profile
except (
AttributeError,
EveCharacter.character_ownership.RelatedObjectDoesNotExist,
CharacterOwnership.user.RelatedObjectDoesNotExist,
):
return None
return userprofile.main_character
def get_user_from_evecharacter(character: EveCharacter) -> User:
"""
Get the user for an EveCharacter or the sentinel user when no user is found
:param character:
:type character:
:return:
:rtype:
"""
try:
userprofile = character.character_ownership.user.profile
except (
AttributeError,
EveCharacter.character_ownership.RelatedObjectDoesNotExist,
CharacterOwnership.user.RelatedObjectDoesNotExist,
):
return get_sentinel_user()
return userprofile.user

View File

@ -1,105 +0,0 @@
"""
Alliance Auth User API
"""
from typing import Optional
from django.contrib.auth.models import User
from allianceauth.authentication.models import CharacterOwnership
from allianceauth.eveonline.models import EveCharacter
def get_all_characters_from_user(user: User, main_first: bool = False) -> list:
"""
Get all characters from a user
This function retrieves all characters associated with a given user, optionally ordering them
with the main character first.
If the user is None, an empty list is returned.
:param user: The user whose characters are to be retrieved
:type user: User
:param main_first: If True, the main character will be listed first
:type main_first: bool
:return: A list of EveCharacter objects associated with the user
:rtype: list[EveCharacter]
"""
if user is None:
return []
try:
if main_first:
characters = [
char.character
for char in CharacterOwnership.objects.filter(user=user).order_by(
"-character__userprofile", "character__character_name"
)
]
else:
characters = [
char.character
for char in CharacterOwnership.objects.filter(user=user).order_by(
"character__character_name"
)
]
except AttributeError:
return []
return characters
def get_main_character_from_user(user: User) -> Optional[EveCharacter]:
"""
Get the main character from a user
:param user:
:type user:
:return:
:rtype:
"""
if user is None:
return None
try:
main_character = user.profile.main_character
except AttributeError:
return None
return main_character
def get_main_character_name_from_user(user: User) -> str:
"""
Get the main character name from a user
:param user:
:type user:
:return:
:rtype:
"""
if user is None:
sentinel_user = get_sentinel_user()
return sentinel_user.username
main_character = get_main_character_from_user(user=user)
try:
username = main_character.character_name
except AttributeError:
return str(user)
return username
def get_sentinel_user() -> User:
"""
Get the sentinel user or create one
:return:
"""
return User.objects.get_or_create(username="deleted")[0]

View File

@ -1,16 +0,0 @@
"""
Framework App Config
"""
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class FrameworkConfig(AppConfig):
"""
Framework App Config
"""
name = "allianceauth.framework"
label = "framework"
verbose_name = _("Framework")

View File

@ -1,174 +0,0 @@
/**
* Alliance Auth CSS Framework
*
* This provides some CSS classes together with a couple of Bootstrap fixes
* to be used throughout Alliance Auth and its Community Apps
*/
/* General
------------------------------------------------------------------------------------- */
@media all {
.navbar-toggler.collapsed {
transform: rotate(180deg);
}
ul#nav-right:has(li) + ul#nav-right-character-control > li:first-child {
display: list-item !important;
}
}
@media all and (max-width: 991px) {
ul#nav-left:has(li) + ul#nav-right + ul#nav-right-character-control > li:first-child {
display: list-item !important;
}
}
/* Bootstrap fixes
------------------------------------------------------------------------------------- */
@media all {
.table {
--bs-table-bg: transparent;
}
}
/* Image overflow fix
------------------------------------------------------------------------------------- */
@media all {
img {
max-width: 100%;
height: auto;
}
}
/* Side Navigation
------------------------------------------------------------------------------------- */
@media all {
#sidebar > div {
width: 325px;
}
/* Menu items in general */
#sidebar-menu li > a,
#sidebar-menu li > ul > li > a {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 210px;
}
/* Parent items with chevron and possible badge */
#sidebar-menu li:has(span.badge) > a[data-bs-toggle="collapse"] {
max-width: 180px;
}
/* Child items with possible badge */
#sidebar-menu li > ul > li > a {
max-width: 189px;
}
/* Chevron icons */
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-down,
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-right {
display: block;
width: 16px;
}
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-right,
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-down {
display: none;
}
}
/* Cursor classes
------------------------------------------------------------------------------------- */
@media all {
.cursor-auto {
cursor: auto;
}
.cursor-default {
cursor: default;
}
.cursor-pointer {
cursor: pointer;
}
.cursor-wait {
cursor: wait;
}
.cursor-text {
cursor: text;
}
.cursor-move {
cursor: move;
}
.cursor-help {
cursor: help;
}
.cursor-not-allowed {
cursor: not-allowed;
}
.cursor-inherit {
cursor: inherit;
}
.cursor-zoom-in {
cursor: zoom-in;
}
.cursor-zoom-out {
cursor: zoom-out;
}
}
/* Callouts
*
* Not quite alerts, but custom and helpful notes for folks.
* Requires a base and modifier class.
------------------------------------------------------------------------------------- */
@media all {
/* Common styles for all types */
.aa-callout {
border: 1px solid var(--bs-border-color);
border-left-width: 0.25rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
padding: 1rem;
}
.aa-callout.aa-callout-sm {
padding: 0.5rem;
}
.aa-callout.aa-callout-lg {
padding: 1.5rem;
}
/* Last item bottom margin should be 0 */
.aa-callout :last-child {
margin-bottom: 0;
}
/* Variations */
.aa-callout.aa-callout-danger {
border-left-color: var(--bs-danger);
}
.aa-callout.aa-callout-info {
border-left-color: var(--bs-info);
}
.aa-callout.aa-callout-success {
border-left-color: var(--bs-success);
}
.aa-callout.aa-callout-warning {
border-left-color: var(--bs-warning);
}
}

View File

@ -1,105 +0,0 @@
"""
Custom static files storage for Alliance Auth.
This module defines a custom static files storage class for
Alliance Auth, named `AaManifestStaticFilesStorage`.
Using `ManifestStaticFilesStorage` will give us a hashed name for
our static files, which is useful for cache busting.
This storage class extends Django's `ManifestStaticFilesStorage` to ignore missing files,
which the original class does not handle, and log them in debug mode.
It is useful for handling cases where static files may not exist, such as when a
CSS file references a background image that is not present in the static files directory.
With debug mode enabled, it will print a message for each missing file when running `collectstatic`,
which can help identify issues with static file references during development.
"""
from django.conf import settings
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
class AaManifestStaticFilesStorage(ManifestStaticFilesStorage):
"""
Custom static files storage that ignores missing files.
"""
@classmethod
def _cleanup_name(cls, name: str) -> str:
"""
Clean up the name by removing quotes.
This method is used to ensure that the name does not contain any quotes,
which can cause issues with file paths.
:param name: The name of the static file.
:type name: str
:return: The cleaned-up name without quotes.
:rtype: str
"""
# Remove quotes from the name
return name.replace('"', "").replace("'", "")
def __init__(self, *args, **kwargs):
"""
Initialize the static files storage, ignoring missing files.
:param args:
:type args:
:param kwargs:
:type kwargs:
"""
self.missing_files = []
super().__init__(*args, **kwargs)
def hashed_name(self, name, content=None, filename=None):
"""
Generate a hashed name for the given static file, ignoring missing files.
Ignore missing files, e.g. non-existent background image referenced from css.
Returns the original filename if the referenced file doesn't exist.
:param name: The name of the static file to hash.
:type name: str
:param content: The content of the static file, if available.
:type content: bytes | None
:param filename: The original filename of the static file, if available.
:type filename: str | None
:return: The hashed name of the static file, or the original name if the file is missing.
:rtype: str
"""
try:
clean_name = self._cleanup_name(name)
return super().hashed_name(clean_name, content, filename)
except ValueError as e:
if settings.DEBUG:
# In debug mode, we log the missing file message
message = e.args[0].split(" with ")[0]
self.missing_files.append(message)
# print(f'\x1b[0;30;41m{message}\x1b[0m')
return name
def post_process(self, *args, **kwargs):
"""
Post-process the static files, printing any missing files in debug mode.
:param args:
:type args:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
yield from super().post_process(*args, **kwargs)
if settings.DEBUG:
# In debug mode, print the missing files
for message in sorted(set(self.missing_files)):
print(f"\x1b[0;30;41m{message}\x1b[0m")

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