Compare commits

..

13 Commits

Author SHA1 Message Date
Ariel Rin
bf2e5adf70 Translate django.po in cs_CZ [Manual Sync]
16% of minimum 1% translated source file: 'django.po'
on 'cs_CZ'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-13 04:15:45 +00:00
Ariel Rin
ecd594a5bb Translate django.po in nl_NL [Manual Sync]
47% of minimum 1% translated source file: 'django.po'
on 'nl_NL'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-13 04:15:06 +00:00
Ariel Rin
8c2648ac7f Translate django.po in de
100% translated source file: 'django.po'
on 'de'.
2024-09-09 17:44:20 +00:00
Ariel Rin
4e901b602b Translate django.po in uk
98% of minimum 50% translated source file: 'django.po'
on 'uk'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:59:20 +00:00
Ariel Rin
1ad5c361f7 Translate django.po in ja
98% of minimum 50% translated source file: 'django.po'
on 'ja'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:59:10 +00:00
Ariel Rin
bbc8998396 Translate django.po in zh-Hans
67% of minimum 50% translated source file: 'django.po'
on 'zh-Hans'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:58:53 +00:00
Ariel Rin
508e701863 Translate django.po in pl_PL
98% of minimum 50% translated source file: 'django.po'
on 'pl_PL'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:58:40 +00:00
Ariel Rin
4539103d70 Translate django.po in it_IT
97% of minimum 50% translated source file: 'django.po'
on 'it_IT'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:58:28 +00:00
Ariel Rin
d5c1f01fd5 Translate django.po in ru
81% of minimum 50% translated source file: 'django.po'
on 'ru'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:58:18 +00:00
Ariel Rin
b2aa8626f3 Translate django.po in fr_FR
98% of minimum 50% translated source file: 'django.po'
on 'fr_FR'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:58:04 +00:00
Ariel Rin
3bd09cee5f Translate django.po in ko_KR
98% of minimum 50% translated source file: 'django.po'
on 'ko_KR'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:57:57 +00:00
Ariel Rin
f453955680 Translate django.po in es
77% of minimum 50% translated source file: 'django.po'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:57:50 +00:00
Ariel Rin
7952956056 Translate django.po in de
98% of minimum 50% translated source file: 'django.po'
on 'de'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2024-09-09 04:57:44 +00:00
509 changed files with 11258 additions and 12431 deletions

View File

@@ -1,6 +0,0 @@
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Ruff initial formatting storm
a99315ea55339f0b6010b5c9d8703e51278fcf29

1
.gitignore vendored
View File

@@ -71,7 +71,6 @@ celerybeat-schedule
#other
.flake8
.ruff_cache
.pylintrc
Makefile
alliance_auth.sqlite3

View File

@@ -25,7 +25,7 @@ before_script:
pre-commit-check:
<<: *only-default
stage: pre-commit
image: python:3.12-bookworm
image: python:3.11-bookworm
# variables:
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
# cache:
@@ -51,6 +51,30 @@ secret_detection:
stage: gitlab
before_script: []
test-3.8-core:
<<: *only-default
image: python:3.8-bookworm
script:
- tox -e py38-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.9-core:
<<: *only-default
image: python:3.9-bookworm
script:
- tox -e py39-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.10-core:
<<: *only-default
image: python:3.10-bookworm
@@ -87,11 +111,23 @@ test-3.12-core:
coverage_format: cobertura
path: coverage.xml
test-3.13-core:
test-3.8-all:
<<: *only-default
image: python:3.13-rc-bookworm
image: python:3.8-bookworm
script:
- tox -e py313-core
- tox -e py38-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.9-all:
<<: *only-default
image: python:3.9-bookworm
script:
- tox -e py39-all
artifacts:
when: always
reports:
@@ -136,21 +172,9 @@ test-3.12-all:
coverage_format: cobertura
path: coverage.xml
test-3.13-all:
<<: *only-default
image: python:3.13-rc-bookworm
script:
- tox -e py313-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
build-test:
stage: test
image: python:3.12-bookworm
image: python:3.11-bookworm
before_script:
- python -m pip install --upgrade pip
@@ -169,13 +193,13 @@ build-test:
test-docs:
<<: *only-default
image: python:3.12-bookworm
image: python:3.11-bookworm
script:
- tox -e docs
deploy_production:
stage: deploy
image: python:3.12-bookworm
image: python:3.11-bookworm
before_script:
- python -m pip install --upgrade pip
@@ -191,10 +215,10 @@ deploy_production:
build-image:
before_script: []
image: docker:27
image: docker:24.0
stage: docker
services:
- docker:27-dind
- docker:24.0-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
@@ -215,10 +239,10 @@ build-image:
build-image-dev:
before_script: []
image: docker:27
image: docker:24.0
stage: docker
services:
- docker:27-dind
- docker:24.0-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
@@ -236,10 +260,10 @@ build-image-dev:
build-image-mr:
before_script: []
image: docker:27
image: docker:24.0
stage: docker
services:
- docker:27-dind
- docker:24.0-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA

View File

@@ -3,42 +3,22 @@
# Update this file:
# pre-commit autoupdate
# Set the default language versions for the hooks
default_language_version:
python: python3 # Force all Python hooks to use Python 3
node: 22.12.0 # Force all Node hooks to use Node 22.12.0
# Globally exclude files
# https://pre-commit.com/#top_level-exclude
exclude: |
(?x)(
LICENSE|
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json|
static/(.*)/libs/|
telnetlib\.py
)
repos:
# Code Upgrades
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.25.0
hooks:
- id: django-upgrade
args: [--target-version=5.2]
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
rev: v3.15.2
hooks:
- id: pyupgrade
args: [--py310-plus]
args: [--py38-plus]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.17.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.6.0
hooks:
# Identify invalid files
- id: check-ast
@@ -53,9 +33,9 @@ repos:
- id: detect-private-key
- id: check-case-conflict
# Python checks
# - id: check-docstring-first
# - id: check-docstring-first
- id: debug-statements
# - id: requirements-txt-fixer
# - id: requirements-txt-fixer
- id: fix-encoding-pragma
args: [--remove]
- id: fix-byte-order-marker
@@ -64,34 +44,56 @@ repos:
args: [--fix=lf]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
exclude: |
(?x)(
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json
)
- id: check-executables-have-shebangs
- id: end-of-file-fixer
exclude: |
(?x)(
\.min\.css|
\.min\.js|
\.po|
\.mo|
swagger\.json
)
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 3.2.1
rev: 2.7.3
hooks:
- id: editorconfig-checker
exclude: |
(?x)(
LICENSE|
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
\.po|
\.mo|
swagger\.json
)
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0
rev: v0.41.0
hooks:
- id: markdownlint
language: node
args:
- --disable=MD013
# Infrastructure
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.6.0
rev: 2.1.3
hooks:
- id: pyproject-fmt
name: pyproject.toml formatter
description: "Format the pyproject.toml file."
args:
- --indent=4
additional_dependencies:
- tox==4.26.0 # https://github.com/tox-dev/tox/releases/latest
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0
hooks:
- id: tox-ini-fmt
- tox==4.15.0 # https://github.com/tox-dev/tox/releases/latest
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.18
hooks:
- id: validate-pyproject
name: Validate pyproject.toml
description: "Validate the pyproject.toml file."

View File

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

View File

@@ -5,7 +5,7 @@
[![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)](https://allianceauth.readthedocs.io/?badge=latest)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest)
[![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk)
@@ -14,7 +14,7 @@ An auth system for EVE Online to help in-game organizations manage online servic
## Content
- [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)
@@ -38,7 +38,7 @@ Main features:
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr:, Russian :flag_ru:, Italian :flag_it:, French :flag_fr:, Japanese :flag_jp: and Ukrainian :flag_ua: Localization
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](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

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__ = '5.0.0a3'
__title__ = 'AllianceAuth'
__version__ = '4.3.0'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}'

View File

@@ -1,17 +1,15 @@
from solo.admin import SingletonModelAdmin
from django.contrib import admin
from .models import AnalyticsIdentifier, AnalyticsTokens
@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,7 @@
# Generated by Django 3.1.4 on 2020-12-30 13:11
import uuid
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):

View File

@@ -21,7 +21,7 @@ def remove_aa_team_token(apps, schema_editor):
# Have to define some code to remove this identifier
# In case of migration rollback?
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
Tokens.objects.filter(token="UA-186249766-2").delete()
token = Tokens.objects.filter(token="UA-186249766-2").delete()
class Migration(migrations.Migration):

View File

@@ -1,5 +1,6 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from uuid import uuid4
from django.db import migrations

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
from django.core.exceptions import ObjectDoesNotExist
def add_aa_team_token(apps, schema_editor):
@@ -51,7 +51,7 @@ def remove_aa_team_token(apps, schema_editor):
# Have to define some code to remove this identifier
# In case of migration rollback?
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
Tokens.objects.filter(token="G-6LYSMYK8DE").delete()
token = Tokens.objects.filter(token="G-6LYSMYK8DE").delete()
class Migration(migrations.Migration):

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import logging
import requests
from celery import shared_task
from django.apps import apps
import logging
from django.conf import settings
from django.apps import apps
from celery import shared_task
from .models import AnalyticsTokens, AnalyticsIdentifier
from .utils import (
install_stat_addons,
install_stat_tokens,
install_stat_users)
from allianceauth import __version__
from .models import AnalyticsIdentifier, AnalyticsTokens
from .utils import existence_baremetal_or_docker, install_stat_addons, install_stat_tokens, install_stat_users
logger = logging.getLogger(__name__)
BASE_URL = "https://www.google-analytics.com"
@@ -67,8 +67,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
@@ -77,7 +77,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',
@@ -85,11 +84,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',
@@ -105,6 +99,7 @@ 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",
@@ -140,7 +135,7 @@ def analytics_daily_stats() -> None:
event_type='Stats')
@shared_task
@shared_task()
def send_ga_tracking_celery_event(
measurement_id: str,
secret: str,
@@ -170,7 +165,7 @@ 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__

View File

@@ -1,8 +1,10 @@
from uuid import uuid4
from allianceauth.analytics.models import AnalyticsIdentifier
from django.core.exceptions import ValidationError
from django.test.testcases import TestCase
from allianceauth.analytics.models import AnalyticsIdentifier
from uuid import UUID, uuid4
# Identifiers
uuid_1 = "ab33e241fbf042b6aa77c7655a768af7"
@@ -12,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

@@ -2,9 +2,12 @@ import requests_mock
from django.test.utils import override_settings
from allianceauth.analytics.tasks import analytics_event, send_ga_tracking_celery_event
from allianceauth.analytics.tasks import (
analytics_event,
send_ga_tracking_celery_event)
from allianceauth.utils.testing import NoSocketsTestCase
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/mp/collect'

View File

@@ -1,8 +1,9 @@
from django.apps import apps
from django.test.testcases import TestCase
from allianceauth.analytics.utils import install_stat_addons, install_stat_users
from allianceauth.authentication.models import User
from esi.models import Token
from allianceauth.analytics.utils import install_stat_users, install_stat_tokens, install_stat_addons
from django.test.testcases import TestCase
def create_testdata():

View File

@@ -1,10 +1,6 @@
import os
from django.apps import apps
from esi.models import Token
from allianceauth.authentication.models import User
from esi.models import Token
def install_stat_users() -> int:
@@ -38,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,4 +1,5 @@
from django.apps import AppConfig
from django.core.checks import Warning, Error, register
class AllianceAuthConfig(AppConfig):

View File

@@ -1,21 +1,43 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group, Permission as BasePermission, User as BaseUser
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission as BasePermission
from django.contrib.auth.models import User as BaseUser
from django.db.models import Count, Q
from django.db.models.functions import Lower
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save
from django.db.models.signals import (
m2m_changed,
post_delete,
post_save,
pre_delete,
pre_save
)
from django.dispatch import receiver
from django.urls import reverse
from django.utils.html import format_html
from django.utils.text import slugify
from allianceauth.authentication.models import CharacterOwnership, OwnershipRecord, State, UserProfile, get_guest_state
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo
from allianceauth.authentication.models import (
CharacterOwnership,
OwnershipRecord,
State,
UserProfile,
get_guest_state
)
from allianceauth.eveonline.models import (
EveAllianceInfo,
EveCharacter,
EveCorporationInfo,
EveFactionInfo
)
from allianceauth.eveonline.tasks import update_character
from allianceauth.hooks import get_hooks
from allianceauth.services.hooks import ServicesHook
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_CHARS, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
from .app_settings import (
AUTHENTICATION_ADMIN_USERS_MAX_CHARS,
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
)
from .forms import UserChangeForm, UserProfileForm
@@ -110,7 +132,10 @@ def user_username(obj):
To be used for all user based admin lists
"""
link = reverse(
f'admin:{obj._meta.app_label}_{type(obj).__name__.lower()}_change',
'admin:{}_{}_change'.format(
obj._meta.app_label,
type(obj).__name__.lower()
),
args=(obj.pk,)
)
user_obj = obj.user if hasattr(obj, 'user') else obj
@@ -523,7 +548,7 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj and obj.pk:
return 'owner_hash', 'character'
return ()
return tuple()
@admin.register(OwnershipRecord)

View File

@@ -25,7 +25,7 @@ def _clean_setting(
if not required_type:
required_type = type(default_value)
if min_value is None and required_type is int:
if min_value is None and required_type == int:
min_value = 0
if (hasattr(settings, name)

View File

@@ -1,5 +1,5 @@
from django.apps import AppConfig
from django.core.checks import Tags, register
from django.core.checks import register, Tags
class AuthenticationConfig(AppConfig):

View File

@@ -1,7 +1,6 @@
from allianceauth import hooks
from allianceauth.hooks import DashboardItemHook
from .views import dashboard_admin, dashboard_characters, dashboard_esi_check, dashboard_groups
from allianceauth import hooks
from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin
class UserCharactersHook(DashboardItemHook):

View File

@@ -1,9 +1,10 @@
import logging
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission, User
from django.contrib.auth.models import User, Permission
from .models import UserProfile, CharacterOwnership, OwnershipRecord
from .models import CharacterOwnership, OwnershipRecord, UserProfile
logger = logging.getLogger(__name__)

View File

@@ -1,5 +1,5 @@
from django.conf import settings
from django.core.checks import Error
from django.conf import settings
def check_login_scopes_setting(*args, **kwargs):

View File

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

View File

@@ -1,11 +1,14 @@
from collections.abc import Callable, Iterable
from django.urls import include
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied
from functools import wraps
from typing import Callable, Iterable, Optional
from django.urls import include
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.urls import include
from django.utils.translation import gettext_lazy as _
@@ -14,7 +17,7 @@ def user_has_main_character(user):
def decorate_url_patterns(
urls, decorator: Callable, excluded_views: Iterable | None = None
urls, decorator: Callable, excluded_views: Optional[Iterable] = None
):
"""Decorate views given in url patterns except when they are explicitly excluded.

View File

@@ -60,7 +60,7 @@ class UserChangeForm(BaseUserChangeForm):
{
"groups": _(
"You are not allowed to add or remove these "
"restricted groups: {}".format(restricted_names)
"restricted groups: %s" % restricted_names
)
}
)

View File

@@ -1,6 +1,5 @@
from django.urls import include, path, re_path
from allianceauth.authentication import views
from django.urls import include, re_path, path
urlpatterns = [
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),

View File

@@ -1,5 +1,4 @@
from django.core.management.base import BaseCommand
from allianceauth.authentication.models import UserProfile
@@ -12,7 +11,8 @@ class Command(BaseCommand):
if profiles.exists():
for profile in profiles:
self.stdout.write(self.style.ERROR(
f'{profile.main_character} does not have an ownership. Resetting user {profile.user} main character.'))
'{} does not have an ownership. Resetting user {} main character.'.format(profile.main_character,
profile.user)))
profile.main_character = None
profile.save()
self.stdout.write(self.style.WARNING(f'Reset {profiles.count()} main characters.'))

View File

@@ -1,7 +1,7 @@
import logging
from django.db import transaction
from django.db.models import Manager, Q, QuerySet
from django.db.models import Manager, QuerySet, Q
from allianceauth.eveonline.models import EveCharacter

View File

@@ -1,8 +1,8 @@
import logging
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
import logging
logger = logging.getLogger(__name__)

View File

@@ -1,8 +1,8 @@
# Generated by Django 1.10.1 on 2016-09-05 21:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):

View File

@@ -2,7 +2,6 @@
from django.db import migrations
def create_permissions(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')

View File

@@ -2,7 +2,6 @@
from django.db import migrations
def delete_permissions(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')

View File

@@ -2,7 +2,6 @@
from django.db import migrations
def count_completed_fields(model):
return len([True for key, value in model.__dict__.items() if bool(value)])

View File

@@ -1,8 +1,8 @@
# Generated by Django 1.10.1 on 2017-01-07 07:11
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):

View File

@@ -1,7 +1,6 @@
# Generated by Django 1.10.5 on 2017-01-12 00:59
from django.db import migrations
from django.db import migrations, models
def remove_permissions(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')

View File

@@ -1,9 +1,9 @@
# Generated by Django 1.10.2 on 2016-12-11 23:14
import logging
from django.db import migrations
import logging
logger = logging.getLogger(__name__)

View File

@@ -1,12 +1,11 @@
# Generated by Django 1.10.5 on 2017-03-22 23:09
import allianceauth.authentication.models
import django.db.models.deletion
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations, models
import allianceauth.authentication.models
def create_guest_state(apps, schema_editor):
State = apps.get_model('authentication', 'State')

View File

@@ -1,8 +1,8 @@
# Generated by Django 2.0.4 on 2018-04-14 18:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def create_initial_records(apps, schema_editor):

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,11 +1,11 @@
import logging
from django.contrib.auth.models import Permission, User
from django.contrib.auth.models import User, Permission
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
from allianceauth.notifications import notify
from django.conf import settings
from .managers import CharacterOwnershipManager, StateManager
@@ -32,7 +32,7 @@ class State(models.Model):
class Meta:
ordering = ['-priority']
def __str__(self) -> str:
def __str__(self):
return self.name
def available_to_character(self, character):
@@ -60,26 +60,25 @@ def get_guest_state_pk():
class UserProfile(models.Model):
class Meta:
default_permissions = ('change',)
class Language(models.TextChoices):
"""
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')
POLISH = 'pl', _("Polish")
user = models.OneToOneField(
User,
@@ -107,15 +106,10 @@ class UserProfile(models.Model):
_("Theme"),
max_length=200,
blank=True,
null=True,
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps"
)
class Meta:
default_permissions = ('change',)
def __str__(self) -> str:
return str(self.user)
def assign_state(self, state=None, commit=True):
if not state:
state = State.objects.get_for_user(self.user)
@@ -126,7 +120,7 @@ class UserProfile(models.Model):
self.save(update_fields=['state'])
notify(
self.user,
_(f'State changed to: {state}'),
_('State changed to: %s' % state),
_('Your user\'s state is now: %(state)s')
% ({'state': state}),
'info'
@@ -141,19 +135,20 @@ class UserProfile(models.Model):
sender=self.__class__, user=self.user, state=self.state
)
def __str__(self):
return str(self.user)
class CharacterOwnership(models.Model):
class Meta:
default_permissions = ('change', 'delete')
ordering = ['user', 'character__character_name']
character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE, related_name='character_ownership')
owner_hash = models.CharField(max_length=28, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
objects = CharacterOwnershipManager()
class Meta:
default_permissions = ('change', 'delete')
ordering = ['user', 'character__character_name']
def __str__(self) -> str:
def __str__(self):
return f"{self.user}: {self.character}"
@@ -166,5 +161,5 @@ class OwnershipRecord(models.Model):
class Meta:
ordering = ['-created']
def __str__(self) -> str:
def __str__(self):
return f"{self.user}: {self.character} on {self.created}"

View File

@@ -1,16 +1,19 @@
import logging
from .models import (
CharacterOwnership,
UserProfile,
get_guest_state,
State,
OwnershipRecord)
from django.contrib.auth.models import User
from django.db.models import Q
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save
from django.dispatch import Signal, receiver
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver, Signal
from esi.models import Token
from allianceauth.eveonline.models import EveCharacter
from .models import CharacterOwnership, OwnershipRecord, State, UserProfile, get_guest_state
logger = logging.getLogger(__name__)
state_changed = Signal()
@@ -105,7 +108,8 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
def validate_main_character(sender, instance, *args, **kwargs):
try:
if instance.user.profile.main_character == instance.character:
logger.info(f"Ownership of a main character {instance.character} has been revoked. Resetting {instance.user} main character.")
logger.info("Ownership of a main character {} has been revoked. Resetting {} main character.".format(
instance.character, instance.user))
# clear main character as user no longer owns them
instance.user.profile.main_character = None
instance.user.profile.save()

View File

@@ -1,7 +1,7 @@
"""Counters for Task Statistics."""
import datetime as dt
from typing import NamedTuple
from typing import NamedTuple, Optional
from .event_series import EventSeries
@@ -16,7 +16,7 @@ class _TaskCounts(NamedTuple):
retried: int
failed: int
total: int
earliest_task: dt.datetime | None
earliest_task: Optional[dt.datetime]
hours: int

View File

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

View File

@@ -1,11 +1,7 @@
"""Signals for Task Statistics."""
from celery.signals import (
task_failure,
task_internal_error,
task_retry,
task_success,
worker_ready,
task_failure, task_internal_error, task_retry, task_success, worker_ready,
)
from django.conf import settings

View File

@@ -4,10 +4,7 @@ from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.counters import (
dashboard_results,
failed_tasks,
retried_tasks,
succeeded_tasks,
dashboard_results, failed_tasks, retried_tasks, succeeded_tasks,
)

View File

@@ -4,8 +4,7 @@ from unittest.mock import patch
from redis import RedisError
from allianceauth.authentication.task_statistics.helpers import (
_RedisStub,
get_redis_client_or_stub,
_RedisStub, get_redis_client_or_stub,
)
MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"

View File

@@ -10,8 +10,8 @@ from allianceauth.authentication.task_statistics.counters import (
succeeded_tasks,
)
from allianceauth.authentication.task_statistics.signals import (
is_enabled,
reset_counters,
is_enabled,
)
from allianceauth.eveonline.tasks import update_character

View File

@@ -1,9 +1,8 @@
import logging
from celery import shared_task
from esi.errors import IncompleteResponseError, TokenExpiredError, TokenInvalidError
from esi.errors import TokenExpiredError, TokenInvalidError, IncompleteResponseError
from esi.models import Token
from celery import shared_task
from allianceauth.authentication.models import CharacterOwnership
@@ -23,7 +22,8 @@ def check_character_ownership(owner_hash):
continue
except (KeyError, IncompleteResponseError):
# We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
logger.warning(f"Failed to validate owner hash of {tokens[0].character_name} due to problems contacting SSO servers.")
logger.warning("Failed to validate owner hash of {} due to problems contacting SSO servers.".format(
tokens[0].character_name))
break
if not t.character_owner_hash == old_hash:
@@ -33,7 +33,7 @@ def check_character_ownership(owner_hash):
break
if not Token.objects.filter(character_owner_hash=owner_hash).exists():
logger.info(f'No tokens found with owner hash {owner_hash}. Revoking ownership.')
logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash)
CharacterOwnership.objects.filter(owner_hash=owner_hash).delete()

View File

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

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base-bs5.html" %}
{% load aa_i18n %}
{% load i18n %}
{% block page_title %}
@@ -14,7 +13,7 @@
{% block content %}
<div>
<p class="mb-3">
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://developers.eveonline.com/authorized-apps where possible."|urlize %}
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
</p>
<table class="table w-100" id="table_tokens">
@@ -51,23 +50,20 @@
{% 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 }}'},
const table = $('#table_tokens').DataTable({
'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;
var api = this.api();
var rows = api.rows({page: 'current'}).nodes();
var last = null;
api.column(grp, {page: 'current'})
.data()
.each((group, i) => {

View File

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

View File

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

View File

@@ -1,6 +1,11 @@
from unittest import mock
from django.db.models.signals import (
m2m_changed,
post_save,
pre_delete,
pre_save
)
from django.urls import reverse
from unittest import mock
MODULE_PATH = 'allianceauth.authentication'
@@ -12,7 +17,9 @@ def patch(target, *args, **kwargs):
def get_admin_change_view_url(obj: object) -> str:
"""returns URL to admin change view for given object"""
return reverse(
f'admin:{obj._meta.app_label}_{type(obj).__name__.lower()}_change',
'admin:{}_{}_change'.format(
obj._meta.app_label, type(obj).__name__.lower()
),
args=(obj.pk,)
)

View File

@@ -5,8 +5,7 @@ from amqp.exceptions import ChannelError
from django.test import TestCase
from allianceauth.authentication.core.celery_workers import (
active_tasks_count,
queued_tasks_count,
active_tasks_count, queued_tasks_count,
)
MODULE_PATH = "allianceauth.authentication.core.celery_workers"

View File

@@ -1,37 +1,42 @@
from unittest.mock import MagicMock, patch
from urllib.parse import quote
from bs4 import BeautifulSoup
from urllib.parse import quote
from unittest.mock import patch, MagicMock
from django_webtest import WebTest
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Group
from django.test import Client, RequestFactory, TestCase
from django.test import TestCase, RequestFactory, Client
from allianceauth.authentication.models import CharacterOwnership, OwnershipRecord, State
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo
from allianceauth.authentication.models import (
CharacterOwnership, State, OwnershipRecord
)
from allianceauth.eveonline.models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
)
from allianceauth.services.hooks import ServicesHook
from allianceauth.tests.auth_utils import AuthUtils
from ..admin import (
BaseUserAdmin,
CharacterOwnershipAdmin,
MainAllianceFilter,
StateAdmin,
MainCorporationsFilter,
MainAllianceFilter,
MainFactionFilter,
OwnershipRecordAdmin,
StateAdmin,
User,
UserAdmin,
make_service_hooks_sync_nickname_action,
make_service_hooks_update_groups_action,
update_main_character_model,
user_main_organization,
user_profile_pic,
user_username,
update_main_character_model,
make_service_hooks_update_groups_action,
make_service_hooks_sync_nickname_action
)
from . import get_admin_change_view_url, get_admin_search_url
MODULE_PATH = 'allianceauth.authentication.admin'
@@ -322,15 +327,15 @@ class TestUserAdmin(TestCaseWithTestData):
def test_user_username_u1(self):
expected = (
f'<strong><a href="/admin/authentication/user/{self.user_1.pk}/change/">'
'Bruce_Wayne</a></strong><br>Bruce Wayne'
'<strong><a href="/admin/authentication/user/{}/change/">'
'Bruce_Wayne</a></strong><br>Bruce Wayne'.format(self.user_1.pk)
)
self.assertEqual(user_username(self.user_1), expected)
def test_user_username_u3(self):
expected = (
f'<strong><a href="/admin/authentication/user/{self.user_3.pk}/change/">'
'Lex_Luthor</a></strong>'
'<strong><a href="/admin/authentication/user/{}/change/">'
'Lex_Luthor</a></strong>'.format(self.user_3.pk)
)
self.assertEqual(user_username(self.user_3), expected)

View File

@@ -1,5 +1,4 @@
from unittest.mock import Mock, patch
from django.test import TestCase
from .. import app_settings
@@ -84,7 +83,7 @@ class TestSetAppSetting(TestCase):
self.assertEqual(result, 50)
@patch(MODULE_PATH + '.app_settings.settings')
def test_default_for_outofrange_int(self, mock_settings):
def test_default_for_invalid_type_int(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 1000
result = app_settings._clean_setting(
'TEST_SETTING_DUMMY',
@@ -97,7 +96,7 @@ class TestSetAppSetting(TestCase):
def test_default_is_none_needs_required_type(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 'invalid type'
with self.assertRaises(ValueError):
app_settings._clean_setting(
result = app_settings._clean_setting(
'TEST_SETTING_DUMMY',
default_value=None
)

View File

@@ -1,13 +1,13 @@
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User, Group
from django.test import TestCase
from esi.models import Token
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
from esi.models import Token
from ..backends import StateBackend
from ..models import CharacterOwnership, OwnershipRecord, UserProfile
from ..models import CharacterOwnership, UserProfile, OwnershipRecord
MODULE_PATH = 'allianceauth.authentication'

View File

@@ -6,11 +6,12 @@ from django.contrib.auth.models import AnonymousUser
from django.http.response import HttpResponse
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls import URLPattern, reverse
from django.urls import reverse, URLPattern
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
from ..decorators import decorate_url_patterns, main_character_required
from ..models import CharacterOwnership
@@ -46,7 +47,7 @@ class DecoratorTestCase(TestCase):
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
self.request.user = AnonymousUser()
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
@@ -54,7 +55,7 @@ class DecoratorTestCase(TestCase):
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
self.request.user = self.no_main_user
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
@@ -62,7 +63,7 @@ class DecoratorTestCase(TestCase):
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
self.request.user = self.main_user
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)

View File

@@ -1,9 +1,9 @@
from unittest.mock import Mock
from django.http import HttpResponse
from django.test.testcases import TestCase
from unittest import mock
from allianceauth.authentication.middleware import UserSettingsMiddleware
from unittest.mock import Mock
from django.http import HttpResponse
from django.test.testcases import TestCase
class TestUserSettingsMiddlewareSaveLang(TestCase):
@@ -39,7 +39,7 @@ class TestUserSettingsMiddlewareSaveLang(TestCase):
of a non-existent (anonymous) user
"""
self.request.user.is_anonymous = True
self.middleware.process_response(
response = self.middleware.process_response(
self.request,
self.response
)
@@ -52,7 +52,7 @@ class TestUserSettingsMiddlewareSaveLang(TestCase):
does the middleware change a language not set in the DB
"""
self.request.user.profile.language = None
self.middleware.process_response(
response = self.middleware.process_response(
self.request,
self.response
)
@@ -64,7 +64,7 @@ class TestUserSettingsMiddlewareSaveLang(TestCase):
"""
Tests the middleware will change a language setting
"""
self.middleware.process_response(
response = self.middleware.process_response(
self.request,
self.response
)
@@ -158,7 +158,7 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase):
tests the middleware will set night_mode if not set
"""
self.request.session = {}
self.middleware.process_response(
response = self.middleware.process_response(
self.request,
self.response
)
@@ -168,7 +168,7 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase):
"""
tests the middleware will set night_mode if set.
"""
self.middleware.process_response(
response = self.middleware.process_response(
self.request,
self.response
)

View File

@@ -3,12 +3,12 @@ from unittest import mock
from django.contrib.auth.models import User
from django.test import TestCase
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo, EveFactionInfo
from allianceauth.tests.auth_utils import AuthUtils
from esi.errors import IncompleteResponseError
from esi.models import Token
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo
from allianceauth.tests.auth_utils import AuthUtils
from ..models import CharacterOwnership, State, get_guest_state
from ..tasks import check_character_ownership

View File

@@ -1,10 +1,19 @@
from django.db.models.signals import post_save
from django.test.testcases import TestCase
from allianceauth.authentication.models import User, UserProfile
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo
from allianceauth.eveonline.models import (
EveCharacter,
EveCorporationInfo,
EveAllianceInfo
)
from django.db.models.signals import (
pre_save,
post_save,
pre_delete,
m2m_changed
)
from allianceauth.tests.auth_utils import AuthUtils
from django.test.testcases import TestCase
from unittest.mock import Mock
from . import patch

View File

@@ -9,12 +9,8 @@ from django.core.cache import cache
from django.test import TestCase
from allianceauth.templatetags.admin_status import (
_current_notifications,
_current_version_summary,
_fetch_list_from_gitlab,
_fetch_notification_issues_from_gitlab,
_latests_versions,
status_overview,
_current_notifications, _current_version_summary, _fetch_list_from_gitlab,
_fetch_notification_issues_from_gitlab, _latests_versions, status_overview,
)
MODULE_PATH = 'allianceauth.templatetags'
@@ -131,7 +127,7 @@ class TestNotifications(TestCase):
# when
result = _current_notifications()
# then
self.assertEqual(result['notifications'], [])
self.assertEqual(result['notifications'], list())
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_is_none(self, mock_cache):
@@ -140,7 +136,7 @@ class TestNotifications(TestCase):
# when
result = _current_notifications()
# then
self.assertEqual(result['notifications'], [])
self.assertEqual(result['notifications'], list())
class TestCeleryQueueLength(TestCase):

View File

@@ -1,13 +1,12 @@
import json
from unittest.mock import patch
import requests_mock
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
from allianceauth.authentication.views import esi_check, task_counts
from allianceauth.authentication.views import task_counts, esi_check
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
MODULE_PATH = "allianceauth.authentication.views"

View File

@@ -38,6 +38,7 @@ 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,11 @@
import logging
import requests
from django_registration.backends.activation.views import (
REGISTRATION_SALT, ActivationView as BaseActivationView,
RegistrationView as BaseRegistrationView,
)
from django_registration.signals import user_registered
from django.conf import settings
from django.contrib import messages
@@ -13,12 +18,6 @@ from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django_registration.backends.activation.views import (
REGISTRATION_SALT,
ActivationView as BaseActivationView,
RegistrationView as BaseRegistrationView,
)
from django_registration.signals import user_registered
from esi.decorators import token_required
from esi.models import Token
@@ -33,7 +32,7 @@ from .models import CharacterOwnership
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
from allianceauth.eveonline.autogroups.models import * # noqa: F403
from allianceauth.eveonline.autogroups.models import * # noqa: F401, F403
else:
_has_auto_groups = False
@@ -88,7 +87,7 @@ def dashboard_esi_check(request):
@login_required
def dashboard(request):
_dash_items = []
_dash_items = list()
hooks = get_hooks('dashboard_hook')
items = [fn() for fn in hooks]
items.sort(key=lambda i: i.order)
@@ -165,7 +164,9 @@ def main_character_change(request, token):
request.user.profile.save(update_fields=['main_character'])
messages.success(request, _('Changed main character to %s') % co.character)
logger.info(
f'Changed user {request.user} main character to {co.character}'
'Changed user {user} main character to {char}'.format(
user=request.user, char=co.character
)
)
return redirect("authentication:dashboard")
@@ -175,9 +176,10 @@ 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, _(
f'Added {token.character_name} to your account.'))
'Added %(name)s to your account.' % ({'name': token.character_name})))
else:
messages.error(request, _(f'Failed to add {token.character_name} to your account: they already have an account.'))
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % (
{'name': token.character_name})))
return redirect('authentication:dashboard')
@@ -292,7 +294,7 @@ class RegistrationView(BaseRegistrationView):
return redirect(settings.LOGIN_URL)
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Keep the request so the user can be automagically logged in.
self.request = request
setattr(self, 'request', request)
return super().dispatch(request, *args, **kwargs)
def register(self, form):
@@ -392,3 +394,12 @@ def esi_check(request) -> JsonResponse:
"data": check_for_override_esi_error_message(_r)
}
return JsonResponse(data)
@login_required
def dashboard_bs3(request):
"""Render dashboard view with BS3 theme.
This is an internal view used for testing BS3 backward compatibility in AA4 only.
"""
return render(request, 'authentication/dashboard_bs3.html')

View File

@@ -2,7 +2,6 @@
import os
import shutil
from optparse import OptionParser
from django.core.management import call_command
from django.core.management.commands.startproject import Command as BaseStartProject
@@ -44,7 +43,7 @@ def create_project(parser, options, args):
# Call the command with extra context
call_command(StartProject(), *args, **command_options)
print(f"Success! {args[0]} has been created.")
print(f"Success! {args[0]} has been created.") # noqa
def update_settings(parser, options, args):
@@ -63,7 +62,7 @@ def update_settings(parser, options, args):
# next check if given path is to the project, so the app is within it
settings_path = os.path.join(project_path, project_name, 'settings/base.py')
if not os.path.exists(settings_path):
parser.error(f"Unable to locate the Alliance Auth project at {project_path}")
parser.error("Unable to locate the Alliance Auth project at %s" % project_path)
# first find the path to the Alliance Auth template settings
import allianceauth

View File

@@ -1,18 +1,12 @@
"""
Django system checks for Alliance Auth
"""
from datetime import datetime, timezone
from sqlite3.dbapi2 import sqlite_version_info
from celery import current_app
from packaging.version import InvalidVersion, Version as Pep440Version
from typing import List
from django import db
from django.conf import settings
from django.core.checks import CheckMessage, Error, Warning, register
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
@@ -21,416 +15,143 @@ 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
def django_settings(app_configs, **kwargs) -> List[CheckMessage]:
errors: List[CheckMessage] = []
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
if settings.SITE_URL[-1] == "/":
errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005"))
else:
errors.append(
Error(
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",
)
)
errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006"))
# Check 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
errors.append(Warning("'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", hint="", id="allianceauth.checks.B007"))
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",
)
)
errors.append(Error("No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", hint="", id="allianceauth.checks.B008"))
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] = []
def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]:
errors: List[CheckMessage] = []
try:
redis_version = Pep440Version(get_redis_client().info()["redis_version"])
redis_version = Pep440Version(get_redis_client().info()['redis_version'])
except InvalidVersion:
errors.append(Warning("Unable to confirm Redis Version"))
return errors
if (
redis_version.major == 7
and redis_version.minor == 2
and datetime.now(timezone.utc)
> datetime(year=2025, month=8, day=31, tzinfo=timezone.utc)
):
errors.append(
Error(
msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested",
hint=allianceauth_redis_install_link,
id="allianceauth.checks.A001",
)
)
if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc):
errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001"))
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",
)
)
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A002"))
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",
)
)
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A018"))
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",
)
)
errors.append(Error(f"Redis {redis_version.public} EOL", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A003"))
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] = []
def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]:
errors: List[CheckMessage] = []
for connection in db.connections.all():
if connection.vendor == "mysql":
try:
mysql_version = Pep440Version(
".".join(str(i) for i in connection.mysql_version)
)
mysql_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
except InvalidVersion:
errors.append(Warning("Unable to confirm MySQL Version"))
return errors
# MySQL 8
if mysql_version.major == 8:
if mysql_version.minor == 4 and datetime.now(timezone.utc) > datetime(
year=2032, month=4, day=30, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MySQL {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A004",
)
)
# Demote versions down here once EOL
elif mysql_version.minor in [1, 2, 3]:
errors.append(
Warning(
msg=f"MySQL {mysql_version.public} Non LTS",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A005",
)
)
elif mysql_version.minor == 0 and datetime.now(timezone.utc) > datetime(
year=2026, month=4, day=30, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MySQL {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A008",
)
)
# MySQL below 8
# This will also catch Mariadb 5.x
elif mysql_version.major < 8:
errors.append(
Error(
msg=f"MySQL or MariaDB {mysql_version.public} EOL",
hint=mysql_quick_guide_link,
id="allianceauth.checks.A009",
)
)
# MySQL 8
if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=timezone.utc):
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004"))
elif mysql_version.major == 8 and mysql_version.minor == 3:
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005"))
elif mysql_version.major == 8 and mysql_version.minor == 2:
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A006"))
elif mysql_version.major == 8 and mysql_version.minor == 1:
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007"))
elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=timezone.utc):
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008"))
elif mysql_version.major < 8: # This will also catch Mariadb 5.x
errors.append(Error(f"MySQL or MariaDB {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A009"))
return errors
@register()
def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: # noqa: C901
"""
Check that MariaDB is a supported version
"""
mariadb_download_link = "https://mariadb.org/download/?t=repo-config"
errors: list[CheckMessage] = []
def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]:
errors: List[CheckMessage] = []
for connection in db.connections.all():
# TODO: Find a way to determine MySQL vs. MariaDB
if connection.vendor == "mysql":
if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB
try:
mariadb_version = Pep440Version(
".".join(str(i) for i in connection.mysql_version)
)
mariadb_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
except InvalidVersion:
errors.append(Warning("Unable to confirm MariaDB Version"))
return errors
# MariaDB 11
if mariadb_version.major == 11:
if mariadb_version.minor == 4 and datetime.now(timezone.utc) > datetime(
year=2029, month=5, day=19, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A010",
)
)
# Demote versions down here once EOL
elif mariadb_version.minor in [0, 1, 2, 3, 5, 6]:
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A013",
)
)
if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010"))
elif mariadb_version.major == 11 and mariadb_version.minor == 2:
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018"))
if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011"))
elif mariadb_version.major == 11 and mariadb_version.minor == 1:
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019"))
if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012"))
elif mariadb_version.major == 11 and mariadb_version.minor in [0, 3]: # Demote versions down here once EOL
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A013"))
# MariaDB 10
elif mariadb_version.major == 10:
if mariadb_version.minor == 11 and datetime.now(timezone.utc) > datetime(
year=2028, month=2, day=10, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A014",
)
)
elif mariadb_version.minor == 6 and datetime.now(timezone.utc) > datetime(
year=2026, month=7, day=6, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A0015",
)
)
elif mariadb_version.minor == 5 and datetime.now(timezone.utc) > datetime(
year=2025, month=6, day=24, tzinfo=timezone.utc
):
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A016",
)
)
# Demote versions down here once EOL
elif mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]:
errors.append(
Error(
msg=f"MariaDB {mariadb_version.public} EOL",
hint=mariadb_download_link,
id="allianceauth.checks.A017",
)
)
elif mariadb_version.major == 10 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014"))
elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015"))
elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=timezone.utc):
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016"))
elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017"))
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] = []
def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]:
errors: List[CheckMessage] = []
for connection in db.connections.all():
if connection.vendor == "sqlite":
try:
sqlite_version = Pep440Version(
".".join(str(i) for i in sqlite_version_info)
)
sqlite_version = Pep440Version(".".join(str(i) for i in sqlite_version_info))
except InvalidVersion:
errors.append(Warning("Unable to confirm SQLite Version"))
return errors
if sqlite_version.major == 3 and sqlite_version.minor < 27:
errors.append(
Error(
msg=f"SQLite {sqlite_version.public} Unsupported by Django",
hint="https://pkgs.org/download/sqlite3",
id="allianceauth.checks.A020",
)
)
errors.append(Error(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] = []
def sql_settings(app_configs, **kwargs) -> List[CheckMessage]:
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",
)
)
errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
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",
)
)
errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
# 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",
# )
# )
# if connection.settings_dict["OPTIONS"]["collation"] != "utf8mb4_unicode_ci":
# errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
# 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",
# )
# )
# errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
# if connection.vendor == "sqlite":
@@ -438,58 +159,20 @@ def sql_settings(app_configs, **kwargs) -> list[CheckMessage]:
@register()
def celery_settings(app_configs, **kwargs) -> list[CheckMessage]:
"""
Check that Celery settings are correctly configured
:param app_configs:
:type app_configs:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
errors: list[CheckMessage] = []
def celery_settings(app_configs, **kwargs) -> List[CheckMessage]:
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",
)
)
if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}:
errors.append(Error("Celery Priorities are not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
except KeyError:
errors.append(
Error(
msg="Celery Priorities are not set",
hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc",
id="allianceauth.checks.B003",
)
)
errors.append(Error("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",
)
)
if current_app.conf.broker_connection_retry_on_startup != True:
errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
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",
)
)
errors.append(Error("Celery broker_connection_retry_on_startup not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
return errors

View File

@@ -1,5 +1,4 @@
from django.conf import settings
from .views import NightModeRedirectView

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import CorpMember, CorpStats
from .models import CorpStats, CorpMember
admin.site.register(CorpStats)
admin.site.register(CorpMember)

View File

@@ -1,9 +1,8 @@
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from allianceauth.corputils import urls
from allianceauth.menu.hooks import MenuItemHook
from allianceauth.services.hooks import UrlHook
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from allianceauth.corputils import urls
class CorpStats(MenuItemHook):

View File

@@ -1,6 +1,5 @@
import logging
from django.db import models
import logging
logger = logging.getLogger(__name__)
@@ -9,7 +8,7 @@ class CorpStatsQuerySet(models.QuerySet):
def visible_to(self, user):
# superusers get all visible
if user.is_superuser:
logger.debug(f'Returning all corpstats for superuser {user}.')
logger.debug('Returning all corpstats for superuser %s.' % user)
return self
try:
@@ -37,7 +36,7 @@ class CorpStatsQuerySet(models.QuerySet):
query |= q
return self.filter(query)
except AssertionError:
logger.debug(f'User {user} has no main character. No corpstats visible.')
logger.debug('User %s has no main character. No corpstats visible.' % user)
return self.none()

View File

@@ -1,7 +1,7 @@
# Generated by Django 1.10.1 on 2016-12-14 21:36
import django.db.models.deletion
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):

View File

@@ -66,11 +66,11 @@ def forward(apps, schema_editor):
g.permissions.add(perm_dict['corpstats']['alliance_apis'].pk)
g.permissions.add(perm_dict['corpstats']['view_alliance_corpstats'].pk)
for _name, perm in perm_dict['user'].items():
for name, perm in perm_dict['user'].items():
perm.delete()
def reverse(apps, schema_editor): # noqa: C901
def reverse(apps, schema_editor):
perm_dict = user_permissions_dict(apps)
corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])

View File

@@ -1,9 +1,8 @@
# Generated by Django 1.10.5 on 2017-03-26 20:13
import json
import django.db.models.deletion
from django.db import migrations, models
import django.db.models.deletion
import json
def convert_json_to_members(apps, schema_editor):

View File

@@ -1,18 +1,16 @@
import logging
import os
from allianceauth.authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden
from django.db import models
from esi.errors import TokenError
from esi.models import Token
from allianceauth.authentication.models import CharacterOwnership, UserProfile
from allianceauth.corputils.managers import CorpStatsManager
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter, EveAllianceInfo
from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger spec operations:
@@ -33,7 +31,6 @@ class CorpStats(models.Model):
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
last_update = models.DateTimeField(auto_now=True)
objects = CorpStatsManager()
class Meta:
permissions = (
('view_corp_corpstats', 'Can view corp stats of their corporation.'),
@@ -43,9 +40,9 @@ class CorpStats(models.Model):
verbose_name = "corp stats"
verbose_name_plural = "corp stats"
objects = CorpStatsManager()
def __str__(self) -> str:
def __str__(self):
return f"{self.__class__.__name__} for {self.corp}"
def update(self):
@@ -79,21 +76,21 @@ class CorpStats(models.Model):
logger.warning(f"{self} failed to update: {e}")
if self.token.user:
notify(
self.token.user, f"{self} failed to update with your ESI token.",
self.token.user, "%s failed to update with your ESI token." % self,
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
level="error")
self.delete()
except HTTPForbidden as e:
logger.warning(f"{self} failed to update: {e}")
if self.token.user:
notify(self.token.user, f"{self} failed to update with your ESI token.", message=f"{e.status_code}: {e.message}", level="error")
notify(self.token.user, "%s failed to update with your ESI token." % self, message=f"{e.status_code}: {e.message}", level="error")
self.delete()
except AssertionError:
logger.warning(f"{self} token character no longer in corp.")
logger.warning("%s token character no longer in corp." % self)
if self.token.user:
notify(
self.token.user, f"{self} cannot update with your ESI token.",
message=f"{self} cannot update with your ESI token as you have left corp.", level="error")
self.token.user, "%s cannot update with your ESI token." % self,
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
self.delete()
@property
@@ -154,7 +151,7 @@ class CorpMember(models.Model):
unique_together = ('corpstats', 'character_id')
ordering = ['character_name']
def __str__(self) -> str:
def __str__(self):
return self.character_name
@property

View File

@@ -1,5 +1,4 @@
from celery import shared_task
from allianceauth.corputils.models import CorpStats

View File

@@ -1,6 +1,5 @@
{% extends 'corputils/base.html' %}
{% load aa_i18n %}
{% load i18n %}
{% load humanize %}
@@ -239,12 +238,9 @@
{% 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] },
],
@@ -253,7 +249,6 @@
});
$('#table-members').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
@@ -264,7 +259,6 @@
});
$('#table-unregistered').DataTable({
"language": {"url": '{{ DT_LANG_PATH }}'},
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },

View File

@@ -1,6 +1,5 @@
{% extends "corputils/base.html" %}
{% load aa_i18n %}
{% load i18n %}
{% block member_data %}
@@ -43,12 +42,9 @@
{% 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
});

View File

@@ -1,18 +1,14 @@
from unittest import mock
from bravado.exception import HTTPForbidden
from django.contrib.auth.models import Permission
from django.test import TestCase
from esi.errors import TokenError
from esi.models import Token
from allianceauth.authentication.models import CharacterOwnership
from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo
from allianceauth.tests.auth_utils import AuthUtils
from .models import CorpMember, CorpStats
from .models import CorpStats, CorpMember
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo, EveCharacter
from esi.models import Token
from esi.errors import TokenError
from bravado.exception import HTTPForbidden
from django.contrib.auth.models import Permission
from allianceauth.authentication.models import CharacterOwnership
class CorpStatsManagerTestCase(TestCase):

View File

@@ -1,5 +1,4 @@
from django.urls import path
from . import views
app_name = 'corputils'

View File

@@ -1,19 +1,16 @@
import os
from bravado.exception import HTTPError
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required, user_passes_test
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.db import IntegrityError
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _
from esi.decorators import token_required
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
from .models import CorpMember, CorpStats
from .models import CorpStats, CorpMember
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
@@ -62,7 +59,7 @@ def corpstats_add(request, token):
@login_required
@user_passes_test(access_corpstats_test)
def corpstats_view(request, corp_id=None): # noqa: C901
def corpstats_view(request, corp_id=None):
corpstats = None
# get requested model

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,14 @@ Admin classes for custom_css app
"""
# Django
from django.contrib import admin
# Django Solos
from solo.admin import SingletonModelAdmin
from django.contrib import admin
from allianceauth.custom_css.forms import CustomCSSAdminForm
# Alliance Auth Custom CSS
from allianceauth.custom_css.models import CustomCSS
from allianceauth.custom_css.forms import CustomCSSAdminForm
@admin.register(CustomCSS)

View File

@@ -3,12 +3,12 @@ Forms for custom_css app
"""
# Alliance Auth Custom CSS
# Django
from django import forms
from allianceauth.custom_css.models import CustomCSS
from allianceauth.custom_css.widgets import CssEditorWidget
# Django
from django import forms
class CustomCSSAdminForm(forms.ModelForm):
"""

View File

@@ -21,6 +21,7 @@ class CustomCSS(SingletonModel):
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."),
)

View File

@@ -3,7 +3,7 @@ Custom template tags for custom_css app
"""
# Alliance Auth Custom CSS
from pathlib import Path
from allianceauth.custom_css.models import CustomCSS
# Django
from django.conf import settings
@@ -11,7 +11,7 @@ from django.template.defaulttags import register
from django.templatetags.static import static
from django.utils.safestring import mark_safe
from allianceauth.custom_css.models import CustomCSS
from pathlib import Path
@register.simple_tag

View File

@@ -6,6 +6,7 @@ Form widgets for custom_css app
from django import forms
# Alliance Auth
from allianceauth.custom_css.models import CustomCSS
class CssEditorWidget(forms.Textarea):

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