mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 06:06:19 +01:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b3077caa | ||
|
|
1786f3a642 | ||
|
|
55927c6f15 | ||
|
|
8fbe0ba45d | ||
|
|
1563805ddb | ||
|
|
c58ed53369 | ||
|
|
32128ace1c | ||
|
|
7290eaad7e | ||
|
|
f23d4f4dd1 | ||
|
|
ab3f10e6f2 | ||
|
|
20187cc73e | ||
|
|
1f55fbfccc | ||
|
|
12383d79c8 | ||
|
|
56e2875650 | ||
|
|
d0118e6c0b | ||
|
|
7075ccdf7a | ||
|
|
b2d540c010 | ||
|
|
7cb7e2c77b | ||
|
|
5d6a4ab1a9 | ||
|
|
1122d617bd | ||
|
|
ef33501e45 | ||
|
|
08fd86db8f | ||
|
|
c4193c15fc | ||
|
|
903074080e | ||
|
|
3046a26a02 | ||
|
|
951c4135c2 | ||
|
|
b256a0c5e1 | ||
|
|
212b9b0f60 | ||
|
|
fc29d7e80d | ||
|
|
ec536c66a0 | ||
|
|
749ece45e2 | ||
|
|
b04c8873d0 | ||
|
|
9a77175bf3 | ||
|
|
5d4c7b9030 | ||
|
|
5f80259d57 | ||
|
|
dcd6bd1b36 | ||
|
|
6f4dffe930 | ||
|
|
56d70e6c74 | ||
|
|
5e14ea4573 | ||
|
|
c743eca0f7 | ||
|
|
2002f24178 | ||
|
|
6412aedf53 | ||
|
|
939df08b95 | ||
|
|
d8506aa753 | ||
|
|
3f2cdac658 | ||
|
|
d57ab01ff3 | ||
|
|
91b62bbe9d | ||
|
|
557a52e3c8 | ||
|
|
f8fefd92a5 | ||
|
|
f2c43ee921 | ||
|
|
99945b0146 | ||
|
|
abb9dc4db6 | ||
|
|
eba5b80cde | ||
|
|
5b39c887a5 | ||
|
|
183363e789 | ||
|
|
d8704f4d8f | ||
|
|
165ee44a63 | ||
|
|
e8f508cecb | ||
|
|
3044f18900 | ||
|
|
1cae20fe5f | ||
|
|
79637020f3 | ||
|
|
2d34422e2d | ||
|
|
6b932b1188 | ||
|
|
f62153c746 | ||
|
|
88216c3f81 | ||
|
|
dc983c31e3 | ||
|
|
4204c44bde | ||
|
|
8da0122d17 | ||
|
|
c9fcf6e6bf | ||
|
|
36866cc59b | ||
|
|
298bdd98ed | ||
|
|
819018748d | ||
|
|
32e8e0fdd0 | ||
|
|
7625060a12 | ||
|
|
672cb13bfe | ||
|
|
7170f75b89 | ||
|
|
8f60c7a00a | ||
|
|
34ae6e402c | ||
|
|
0905e48994 | ||
|
|
02fcf7d500 | ||
|
|
8d8da50946 | ||
|
|
c1499d173f | ||
|
|
b149baa4e5 | ||
|
|
4807c69b5e | ||
|
|
ebefa0e307 | ||
|
|
468e7433f9 | ||
|
|
3ca313f907 | ||
|
|
820065fc04 | ||
|
|
3eddeefe28 | ||
|
|
82d7d7e3bf | ||
|
|
93194b4f2d | ||
|
|
fa335253d3 | ||
|
|
d1af9416b3 | ||
|
|
f4ac2ea400 | ||
|
|
31c1f8bb7d | ||
|
|
57f7178f1e | ||
|
|
17d4a4c415 | ||
|
|
18ce433fa0 | ||
|
|
1eadb1d934 | ||
|
|
59a8f8a967 | ||
|
|
2dc07b5519 | ||
|
|
3454520dfe | ||
|
|
7a195d4158 | ||
|
|
b73072dec0 | ||
|
|
1ca5e38bd9 | ||
|
|
ecb737c6a5 | ||
|
|
7063f53cdf | ||
|
|
017424b9d4 | ||
|
|
f6c26cf2ec | ||
|
|
9a422bd4ca | ||
|
|
47fec23f2e | ||
|
|
399ef1917d | ||
|
|
9db443ba54 | ||
|
|
0f2f5ea0ba | ||
|
|
1f781c5037 | ||
|
|
36dedfcbd2 | ||
|
|
13a05606fb | ||
|
|
90ad7790e1 | ||
|
|
6b8341ab5a | ||
|
|
d15f42b3fd | ||
|
|
cc60b26f5a | ||
|
|
36ff0af993 | ||
|
|
f17c94a9e1 | ||
|
|
7e3ba476f3 | ||
|
|
dd1313a2a9 | ||
|
|
763003bd7d | ||
|
|
f3217443dd | ||
|
|
a713ae1914 | ||
|
|
5815bac0df | ||
|
|
6154d2c2e7 | ||
|
|
b34661b35d | ||
|
|
a9a7e03b80 | ||
|
|
23c797ef64 | ||
|
|
da102618a0 | ||
|
|
51ee281b14 | ||
|
|
9133232c20 | ||
|
|
9cbabee126 | ||
|
|
4026523a2e | ||
|
|
7fbf96623b | ||
|
|
273bda173e | ||
|
|
7bd5838ea1 | ||
|
|
b232d9ab17 | ||
|
|
a11b870664 | ||
|
|
a27aae5d1c | ||
|
|
117ef63d90 | ||
|
|
1bde3d5672 | ||
|
|
d2355b1ec8 | ||
|
|
191d474a8e | ||
|
|
ec9a9733be | ||
|
|
cf7a8cedf1 | ||
|
|
18cbb994d5 | ||
|
|
663388a0c2 | ||
|
|
7a943591ec | ||
|
|
cd189927fe | ||
|
|
8772349309 | ||
|
|
cf20100cb5 | ||
|
|
9b9c2ddc04 | ||
|
|
34839e8344 | ||
|
|
89ef4f4cbc | ||
|
|
2cc7f46aae | ||
|
|
8d255fb720 | ||
|
|
67cf68ad87 | ||
|
|
db1971d4c2 | ||
|
|
63c1521cba | ||
|
|
ba7ef11505 | ||
|
|
d2e494b9be | ||
|
|
98bab0b180 | ||
|
|
c4efb2a11f | ||
|
|
94e4895f29 | ||
|
|
70eb1b5b50 | ||
|
|
e247a94db3 | ||
|
|
714431c932 | ||
|
|
b026277ab0 | ||
|
|
11855f0b54 | ||
|
|
635fbfe2c8 | ||
|
|
b10233daf0 | ||
|
|
1aa3187491 | ||
|
|
59f17a88f0 | ||
|
|
75db3195d4 | ||
|
|
afe3fea757 | ||
|
|
1072c00a28 | ||
|
|
b221c1ce24 | ||
|
|
1617c775ee | ||
|
|
cc88a02001 | ||
|
|
ae5d0f4a2f | ||
|
|
067e2c424e | ||
|
|
d64675a3b0 | ||
|
|
17a6b3225e | ||
|
|
b83f591dc2 | ||
|
|
74651dd30a |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -69,11 +69,7 @@ celerybeat-schedule
|
||||
#gitlab configs
|
||||
.gitlab/
|
||||
|
||||
#transifex
|
||||
.tx/
|
||||
|
||||
#other
|
||||
.flake8
|
||||
.pylintrc
|
||||
Makefile
|
||||
.isort.cfg
|
||||
|
||||
@@ -25,7 +25,7 @@ before_script:
|
||||
pre-commit-check:
|
||||
<<: *only-default
|
||||
stage: pre-commit
|
||||
image: python:3.8-bullseye
|
||||
image: python:3.10-bullseye
|
||||
variables:
|
||||
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||
cache:
|
||||
@@ -89,7 +89,7 @@ test-3.10-core:
|
||||
|
||||
test-3.11-core:
|
||||
<<: *only-default
|
||||
image: python:3.11-rc-bullseye
|
||||
image: python:3.11-bullseye
|
||||
script:
|
||||
- tox -e py311-core
|
||||
artifacts:
|
||||
@@ -98,6 +98,18 @@ test-3.11-core:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
test-pvpy-core:
|
||||
<<: *only-default
|
||||
image: pypy:3.9-bullseye
|
||||
script:
|
||||
- tox -e pypy-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
test-3.8-all:
|
||||
@@ -138,7 +150,7 @@ test-3.10-all:
|
||||
|
||||
test-3.11-all:
|
||||
<<: *only-default
|
||||
image: python:3.11-rc-bullseye
|
||||
image: python:3.11-bullseye
|
||||
script:
|
||||
- tox -e py311-all
|
||||
artifacts:
|
||||
@@ -147,6 +159,19 @@ test-3.11-all:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
|
||||
test-pvpy-all:
|
||||
<<: *only-default
|
||||
image: pypy:3.9-bullseye
|
||||
script:
|
||||
- tox -e pypy-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
build-test:
|
||||
|
||||
@@ -5,35 +5,75 @@
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-json
|
||||
- id: check-xml
|
||||
# Identify invalid files
|
||||
- id: check-ast
|
||||
- id: check-yaml
|
||||
- id: fix-byte-order-marker
|
||||
- id: trailing-whitespace
|
||||
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
|
||||
- id: end-of-file-fixer
|
||||
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
|
||||
- id: mixed-line-ending
|
||||
args: [ '--fix=lf' ]
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-xml
|
||||
|
||||
# git checks
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
args: [ --maxkb=1000 ]
|
||||
- id: detect-private-key
|
||||
- id: check-case-conflict
|
||||
|
||||
# Python checks
|
||||
# - id: check-docstring-first
|
||||
- id: debug-statements
|
||||
# - id: requirements-txt-fixer
|
||||
- id: fix-encoding-pragma
|
||||
args: [ '--remove' ]
|
||||
args: [ --remove ]
|
||||
- id: fix-byte-order-marker
|
||||
|
||||
# General quality checks
|
||||
- id: mixed-line-ending
|
||||
args: [ --fix=lf ]
|
||||
- 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: 2.4.0
|
||||
rev: 2.7.2
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
exclude: ^(LICENSE|allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
|
||||
exclude: |
|
||||
(?x)(
|
||||
LICENSE|
|
||||
allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|
|
||||
\.po|
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.14.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [ --target-version=4.0 ]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.34.0
|
||||
rev: v3.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [ --py38-plus ]
|
||||
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.1
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
|
||||
10
.tx/config
Normal file
10
.tx/config
Normal file
@@ -0,0 +1,10 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = zh-Hans: zh_Hans
|
||||
|
||||
[o:alliance-auth:p:alliance-auth:r:django-po]
|
||||
file_filter = allianceauth/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_file = allianceauth/locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
minimum_perc = 0
|
||||
10
.tx/config_20230406134150.bak
Normal file
10
.tx/config_20230406134150.bak
Normal file
@@ -0,0 +1,10 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = zh-Hans:zh_Hans
|
||||
|
||||
[alliance-auth.django-po]
|
||||
file_filter = allianceauth/locale/<lang>/LC_MESSAGES/django.po
|
||||
minimum_perc = 0
|
||||
source_file = allianceauth/locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
6
README.md
Executable file → Normal file
6
README.md
Executable file → Normal file
@@ -36,7 +36,7 @@ Main features:
|
||||
|
||||
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
||||
|
||||
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr: and Russian :flag_ru: localization
|
||||
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr:, Russian :flag_ru:, Italian :flag_it:, French :flag_fr:, Japanese :flag_jp: and Ukrainian :flag_ua: Localization
|
||||
|
||||
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
|
||||
|
||||
@@ -56,13 +56,15 @@ Here is an example of the Alliance Auth web site with some plug-ins apps and ser
|
||||
|
||||
- [Aaron Kable](https://gitlab.com/aaronkable/)
|
||||
- [Ariel Rin](https://gitlab.com/soratidus999/)
|
||||
- [Basraah](https://gitlab.com/basraah/)
|
||||
- [Col Crunch](https://gitlab.com/colcrunch/)
|
||||
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
|
||||
- [Rounon Dax](https://gitlab.com/ppfeufer)
|
||||
- [snipereagle1](https://gitlab.com/mckernanin)
|
||||
|
||||
### Former Developers
|
||||
|
||||
- [Adarnof](https://gitlab.com/adarnof/)
|
||||
- [Basraah](https://gitlab.com/basraah/)
|
||||
|
||||
### Beta Testers / Bug Fixers
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""An auth system for EVE Online to help in-game organizations
|
||||
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__ = '3.0.0'
|
||||
__version__ = '3.6.1'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = f'{__title__} v{__version__}'
|
||||
|
||||
@@ -8,13 +8,13 @@ from uuid import uuid4
|
||||
class AnalyticsIdentifier(models.Model):
|
||||
|
||||
identifier = models.UUIDField(default=uuid4,
|
||||
editable=False)
|
||||
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')
|
||||
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)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib import messages
|
||||
|
||||
from .models import UserProfile, CharacterOwnership, OwnershipRecord
|
||||
|
||||
@@ -37,7 +38,13 @@ class StateBackend(ModelBackend):
|
||||
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
|
||||
if ownership.owner_hash == token.character_owner_hash:
|
||||
logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}')
|
||||
return ownership.user
|
||||
if ownership.user.profile.main_character:
|
||||
if ownership.user.profile.main_character.character_id == token.character_id:
|
||||
return ownership.user
|
||||
else: ## this is an alt, enforce main only.
|
||||
if request:
|
||||
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account.")
|
||||
return None
|
||||
else:
|
||||
logger.debug(f'{token.character_name} has changed ownership. Creating new user account.')
|
||||
ownership.delete()
|
||||
@@ -57,13 +64,20 @@ class StateBackend(ModelBackend):
|
||||
if records.exists():
|
||||
# we've seen this character owner before. Re-attach to their old user account
|
||||
user = records[0].user
|
||||
if user.profile.main_character:
|
||||
if ownership.user.profile.main_character.character_id != token.character_id:
|
||||
## this is an alt, enforce main only due to trust issues in SSO.
|
||||
if request:
|
||||
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.")
|
||||
return None
|
||||
|
||||
token.user = user
|
||||
co = CharacterOwnership.objects.create_by_token(token)
|
||||
logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}')
|
||||
if not user.profile.main_character:
|
||||
# set this as their main by default if they have none
|
||||
user.profile.main_character = co.character
|
||||
user.profile.save()
|
||||
|
||||
# set this as their main by default as they have none
|
||||
user.profile.main_character = co.character
|
||||
user.profile.save()
|
||||
return user
|
||||
logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.')
|
||||
return self.create_user(token)
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
from django.conf.urls import include
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from functools import wraps
|
||||
from django.shortcuts import redirect
|
||||
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.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
def user_has_main_character(user):
|
||||
return bool(user.profile.main_character)
|
||||
|
||||
|
||||
def decorate_url_patterns(urls, decorator):
|
||||
def decorate_url_patterns(
|
||||
urls, decorator: Callable, excluded_views: Optional[Iterable] = None
|
||||
):
|
||||
"""Decorate views given in url patterns except when they are explicitly excluded.
|
||||
|
||||
Args:
|
||||
- urls: Django URL patterns
|
||||
- decorator: Decorator to be added to each view
|
||||
- exclude_views: Optional iterable of view names to be excluded
|
||||
"""
|
||||
url_list, app_name, namespace = include(urls)
|
||||
|
||||
def process_patterns(url_patterns):
|
||||
@@ -22,6 +32,8 @@ def decorate_url_patterns(urls, decorator):
|
||||
process_patterns(pattern.url_patterns)
|
||||
else:
|
||||
# this is a pattern
|
||||
if excluded_views and pattern.lookup_str in excluded_views:
|
||||
return
|
||||
pattern.callback = decorator(pattern.callback)
|
||||
|
||||
process_patterns(url_list)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
from django.conf.urls import include
|
||||
|
||||
from allianceauth.authentication import views
|
||||
from django.urls import re_path
|
||||
from django.urls import path
|
||||
from django.urls import include, re_path, path
|
||||
|
||||
urlpatterns = [
|
||||
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),
|
||||
|
||||
0
allianceauth/authentication/managers.py
Executable file → Normal file
0
allianceauth/authentication/managers.py
Executable file → Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.0.10 on 2023-05-28 15:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentication", "0020_userprofile_language_userprofile_night_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en", "English"),
|
||||
("de", "German"),
|
||||
("es", "Spanish"),
|
||||
("zh-hans", "Chinese Simplified"),
|
||||
("ru", "Russian"),
|
||||
("ko", "Korean"),
|
||||
("fr", "French"),
|
||||
("ja", "Japanese"),
|
||||
("it", "Italian"),
|
||||
("uk", "Ukrainian"),
|
||||
],
|
||||
default="",
|
||||
max_length=10,
|
||||
verbose_name="Language",
|
||||
),
|
||||
),
|
||||
]
|
||||
37
allianceauth/authentication/models.py
Executable file → Normal file
37
allianceauth/authentication/models.py
Executable file → Normal file
@@ -18,13 +18,13 @@ class State(models.Model):
|
||||
priority = models.IntegerField(unique=True, help_text="Users get assigned the state with the highest priority available to them.")
|
||||
|
||||
member_characters = models.ManyToManyField(EveCharacter, blank=True,
|
||||
help_text="Characters to which this state is available.")
|
||||
help_text="Characters to which this state is available.")
|
||||
member_corporations = models.ManyToManyField(EveCorporationInfo, blank=True,
|
||||
help_text="Corporations to whose members this state is available.")
|
||||
help_text="Corporations to whose members this state is available.")
|
||||
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
|
||||
help_text="Alliances to whose members this state is available.")
|
||||
help_text="Alliances to whose members this state is available.")
|
||||
member_factions = models.ManyToManyField(EveFactionInfo, blank=True,
|
||||
help_text="Factions to whose members this state is available.")
|
||||
help_text="Factions to whose members this state is available.")
|
||||
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
||||
|
||||
objects = StateManager()
|
||||
@@ -63,6 +63,22 @@ class UserProfile(models.Model):
|
||||
class Meta:
|
||||
default_permissions = ('change',)
|
||||
|
||||
class Language(models.TextChoices):
|
||||
"""
|
||||
Choices for UserProfile.language
|
||||
"""
|
||||
|
||||
ENGLISH = 'en', _('English')
|
||||
GERMAN = 'de', _('German')
|
||||
SPANISH = 'es', _('Spanish')
|
||||
CHINESE = 'zh-hans', _('Chinese Simplified')
|
||||
RUSSIAN = 'ru', _('Russian')
|
||||
KOREAN = 'ko', _('Korean')
|
||||
FRENCH = 'fr', _('French')
|
||||
JAPANESE = 'ja', _('Japanese')
|
||||
ITALIAN = 'it', _('Italian')
|
||||
UKRAINIAN = 'uk', _('Ukrainian')
|
||||
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
related_name='profile',
|
||||
@@ -76,20 +92,9 @@ class UserProfile(models.Model):
|
||||
State,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=get_guest_state_pk)
|
||||
LANGUAGE_CHOICES = [
|
||||
('en', _('English')),
|
||||
('de', _('German')),
|
||||
('es', _('Spanish')),
|
||||
('zh-hans', _('Chinese Simplified')),
|
||||
('ru', _('Russian')),
|
||||
('ko', _('Korean')),
|
||||
('fr', _('French')),
|
||||
('ja', _('Japanese')),
|
||||
('it', _('Italian')),
|
||||
]
|
||||
language = models.CharField(
|
||||
_("Language"), max_length=10,
|
||||
choices=LANGUAGE_CHOICES,
|
||||
choices=Language.choices,
|
||||
blank=True,
|
||||
default='')
|
||||
night_mode = models.BooleanField(
|
||||
|
||||
@@ -6,24 +6,26 @@ CSS for allianceauth admin site
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.column-user_profile_pic {
|
||||
width: 1px;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/* tooltip */
|
||||
.tooltip {
|
||||
position: relative ;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip:hover::after {
|
||||
content: attr(data-tooltip) ;
|
||||
position: absolute ;
|
||||
top: 1.1em ;
|
||||
left: 1em ;
|
||||
min-width: 200px ;
|
||||
border: 1px #808080 solid ;
|
||||
padding: 8px ;
|
||||
color: black ;
|
||||
background-color: rgb(255, 255, 204) ;
|
||||
z-index: 1 ;
|
||||
background-color: rgb(255 255 204);
|
||||
border: 1px rgb(128 128 128) solid;
|
||||
color: rgb(0 0 0);
|
||||
content: attr(data-tooltip);
|
||||
left: 1em;
|
||||
min-width: 200px;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
top: 1.1em;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,44 @@
|
||||
from collections import namedtuple
|
||||
"""Counters for Task Statistics."""
|
||||
|
||||
import datetime as dt
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from .event_series import EventSeries
|
||||
from .helpers import ItemCounter
|
||||
|
||||
|
||||
"""Global series for counting task events."""
|
||||
# Global series for counting task events.
|
||||
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
|
||||
retried_tasks = EventSeries("RETRIED_TASKS")
|
||||
failed_tasks = EventSeries("FAILED_TASKS")
|
||||
running_tasks = ItemCounter("running_tasks")
|
||||
|
||||
|
||||
_TaskCounts = namedtuple(
|
||||
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
|
||||
)
|
||||
class _TaskCounts(NamedTuple):
|
||||
succeeded: int
|
||||
retried: int
|
||||
failed: int
|
||||
total: int
|
||||
earliest_task: Optional[dt.datetime]
|
||||
hours: int
|
||||
running: int
|
||||
|
||||
|
||||
def dashboard_results(hours: int) -> _TaskCounts:
|
||||
"""Counts of all task events within the given timeframe."""
|
||||
"""Counts of all task events within the given time frame."""
|
||||
|
||||
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
|
||||
my_earliest = events.first_event(earliest=earliest)
|
||||
return [my_earliest] if my_earliest else []
|
||||
|
||||
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||
earliest_events = list()
|
||||
earliest_events = []
|
||||
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
||||
retried_count = retried_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(retried_tasks, earliest)
|
||||
failed_count = failed_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(failed_tasks, earliest)
|
||||
running_count = running_tasks.value()
|
||||
return _TaskCounts(
|
||||
succeeded=succeeded_count,
|
||||
retried=retried_count,
|
||||
@@ -37,4 +46,5 @@ def dashboard_results(hours: int) -> _TaskCounts:
|
||||
total=succeeded_count + retried_count + failed_count,
|
||||
earliest_task=min(earliest_events) if earliest_events else None,
|
||||
hours=hours,
|
||||
running=running_count,
|
||||
)
|
||||
|
||||
@@ -1,61 +1,31 @@
|
||||
"""Event series for Task Statistics."""
|
||||
|
||||
import datetime as dt
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from pytz import utc
|
||||
from redis import Redis, RedisError
|
||||
from redis import Redis
|
||||
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
from .helpers import get_redis_client_or_stub
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _RedisStub:
|
||||
"""Stub of a Redis client.
|
||||
|
||||
It's purpose is to prevent EventSeries objects from trying to access Redis
|
||||
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
||||
"""
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def incr(self, *args, **kwargs):
|
||||
return 0
|
||||
|
||||
def zadd(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zcount(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zrangebyscore(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class EventSeries:
|
||||
"""API for recording and analyzing a series of events."""
|
||||
|
||||
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
||||
|
||||
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
||||
self._redis = get_redis_client() if not redis else redis
|
||||
try:
|
||||
if not self._redis.ping():
|
||||
raise RuntimeError()
|
||||
except (AttributeError, RedisError, RuntimeError):
|
||||
logger.exception(
|
||||
"Failed to establish a connection with Redis. "
|
||||
"This EventSeries object is disabled.",
|
||||
)
|
||||
self._redis = _RedisStub()
|
||||
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()
|
||||
|
||||
@property
|
||||
def is_disabled(self):
|
||||
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
||||
return isinstance(self._redis, _RedisStub)
|
||||
return hasattr(self._redis, "IS_STUB")
|
||||
|
||||
@property
|
||||
def _key_counter(self):
|
||||
@@ -73,8 +43,8 @@ class EventSeries:
|
||||
"""
|
||||
if not event_time:
|
||||
event_time = dt.datetime.utcnow()
|
||||
id = self._redis.incr(self._key_counter)
|
||||
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
|
||||
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]:
|
||||
"""List of all known events."""
|
||||
@@ -95,15 +65,15 @@ class EventSeries:
|
||||
self._redis.delete(self._key_counter)
|
||||
|
||||
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
|
||||
"""Count of events, can be restricted to given timeframe.
|
||||
"""Count of events, can be restricted to given time frame.
|
||||
|
||||
Args:
|
||||
- earliest: Date of first events to count(inclusive), or -infinite if not specified
|
||||
- latest: Date of last events to count(inclusive), or +infinite if not specified
|
||||
"""
|
||||
min = "-inf" if not earliest else earliest.timestamp()
|
||||
max = "+inf" if not latest else latest.timestamp()
|
||||
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
|
||||
minimum = "-inf" if not earliest else earliest.timestamp()
|
||||
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) -> Optional[dt.datetime]:
|
||||
"""Date/Time of first event. Returns `None` if series has no events.
|
||||
@@ -111,10 +81,10 @@ class EventSeries:
|
||||
Args:
|
||||
- earliest: Date of first events to count(inclusive), or any if not specified
|
||||
"""
|
||||
min = "-inf" if not earliest else earliest.timestamp()
|
||||
minimum = "-inf" if not earliest else earliest.timestamp()
|
||||
event = self._redis.zrangebyscore(
|
||||
self._key_sorted_set,
|
||||
min,
|
||||
minimum,
|
||||
"+inf",
|
||||
withscores=True,
|
||||
start=0,
|
||||
|
||||
108
allianceauth/authentication/task_statistics/helpers.py
Normal file
108
allianceauth/authentication/task_statistics/helpers.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Helpers for Task Statistics."""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from redis import Redis, RedisError
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _RedisStub:
|
||||
"""Stub of a Redis client.
|
||||
|
||||
It's purpose is to prevent EventSeries objects from trying to access Redis
|
||||
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
||||
"""
|
||||
|
||||
IS_STUB = True
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def incr(self, *args, **kwargs):
|
||||
return 0
|
||||
|
||||
def zadd(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zcount(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zrangebyscore(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class ItemCounter:
|
||||
"""A process safe item counter.
|
||||
|
||||
Args:
|
||||
- name: Unique name for the counter
|
||||
- minimum: Counter can not go below the minimum, when set
|
||||
- redis: A Redis client. Will use AA's cache client by default
|
||||
"""
|
||||
|
||||
CACHE_KEY_BASE = "allianceauth-item-counter"
|
||||
DEFAULT_CACHE_TIMEOUT = 24 * 3600
|
||||
|
||||
def __init__(
|
||||
self, name: str, minimum: Optional[int] = None, redis: Optional[Redis] = None
|
||||
) -> None:
|
||||
if not name:
|
||||
raise ValueError("Must define a name")
|
||||
|
||||
self._name = str(name)
|
||||
self._minimum = minimum
|
||||
self._redis = get_redis_client_or_stub() if not redis else redis
|
||||
|
||||
@property
|
||||
def _cache_key(self) -> str:
|
||||
return f"{self.CACHE_KEY_BASE}-{self._name}"
|
||||
|
||||
def reset(self, init_value: int = 0):
|
||||
"""Reset counter to initial value."""
|
||||
with self._redis.lock(f"{self.CACHE_KEY_BASE}-reset"):
|
||||
if self._minimum is not None and init_value < self._minimum:
|
||||
raise ValueError("Can not reset below minimum")
|
||||
|
||||
cache.set(self._cache_key, init_value, self.DEFAULT_CACHE_TIMEOUT)
|
||||
|
||||
def incr(self, delta: int = 1):
|
||||
"""Increment counter by delta."""
|
||||
try:
|
||||
cache.incr(self._cache_key, delta)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def decr(self, delta: int = 1):
|
||||
"""Decrement counter by delta."""
|
||||
with self._redis.lock(f"{self.CACHE_KEY_BASE}-decr"):
|
||||
if self._minimum is not None and self.value() == self._minimum:
|
||||
return
|
||||
try:
|
||||
cache.decr(self._cache_key, delta)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def value(self) -> Optional[int]:
|
||||
"""Return current value or None if not yet initialized."""
|
||||
return cache.get(self._cache_key)
|
||||
|
||||
|
||||
def get_redis_client_or_stub() -> Redis:
|
||||
"""Return AA's default cache client or a stub if Redis is not available."""
|
||||
redis = get_redis_client()
|
||||
try:
|
||||
if not redis.ping():
|
||||
raise RuntimeError()
|
||||
except (AttributeError, RedisError, RuntimeError):
|
||||
logger.exception(
|
||||
"Failed to establish a connection with Redis. "
|
||||
"This EventSeries object is disabled.",
|
||||
)
|
||||
return _RedisStub()
|
||||
return redis
|
||||
@@ -1,14 +1,15 @@
|
||||
"""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_postrun, task_prerun, task_retry,
|
||||
task_success, worker_ready,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .counters import failed_tasks, retried_tasks, succeeded_tasks
|
||||
from .counters import (
|
||||
failed_tasks, retried_tasks, running_tasks, succeeded_tasks,
|
||||
)
|
||||
|
||||
|
||||
def reset_counters():
|
||||
@@ -16,9 +17,11 @@ def reset_counters():
|
||||
succeeded_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
running_tasks.reset()
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""Return True if task statistics are enabled, else return False."""
|
||||
return not bool(
|
||||
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
|
||||
)
|
||||
@@ -52,3 +55,15 @@ def record_task_failed(*args, **kwargs):
|
||||
def record_task_internal_error(*args, **kwargs):
|
||||
if is_enabled():
|
||||
failed_tasks.add()
|
||||
|
||||
|
||||
@task_prerun.connect
|
||||
def record_task_prerun(*args, **kwargs):
|
||||
if is_enabled():
|
||||
running_tasks.incr()
|
||||
|
||||
|
||||
@task_postrun.connect
|
||||
def record_task_postrun(*args, **kwargs):
|
||||
if is_enabled():
|
||||
running_tasks.decr()
|
||||
|
||||
@@ -8,25 +8,31 @@ from allianceauth.authentication.task_statistics.counters import (
|
||||
succeeded_tasks,
|
||||
retried_tasks,
|
||||
failed_tasks,
|
||||
running_tasks,
|
||||
)
|
||||
|
||||
|
||||
class TestDashboardResults(TestCase):
|
||||
def test_should_return_counts_for_given_timeframe_only(self):
|
||||
def test_should_return_counts_for_given_time_frame_only(self):
|
||||
# given
|
||||
earliest_task = now() - dt.timedelta(minutes=15)
|
||||
|
||||
succeeded_tasks.clear()
|
||||
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
succeeded_tasks.add(earliest_task)
|
||||
succeeded_tasks.add()
|
||||
succeeded_tasks.add()
|
||||
|
||||
retried_tasks.clear()
|
||||
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
retried_tasks.add(now() - dt.timedelta(seconds=30))
|
||||
retried_tasks.add()
|
||||
|
||||
failed_tasks.clear()
|
||||
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
failed_tasks.add()
|
||||
|
||||
running_tasks.reset(8)
|
||||
# when
|
||||
results = dashboard_results(hours=1)
|
||||
# then
|
||||
@@ -35,12 +41,14 @@ class TestDashboardResults(TestCase):
|
||||
self.assertEqual(results.failed, 1)
|
||||
self.assertEqual(results.total, 6)
|
||||
self.assertEqual(results.earliest_task, earliest_task)
|
||||
self.assertEqual(results.running, 8)
|
||||
|
||||
def test_should_work_with_no_data(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
running_tasks.reset()
|
||||
# when
|
||||
results = dashboard_results(hours=1)
|
||||
# then
|
||||
@@ -49,3 +57,4 @@ class TestDashboardResults(TestCase):
|
||||
self.assertEqual(results.failed, 0)
|
||||
self.assertEqual(results.total, 0)
|
||||
self.assertIsNone(results.earliest_task)
|
||||
self.assertEqual(results.running, 0)
|
||||
|
||||
@@ -1,48 +1,19 @@
|
||||
import datetime as dt
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytz import utc
|
||||
from redis import RedisError
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.task_statistics.event_series import (
|
||||
EventSeries,
|
||||
_RedisStub,
|
||||
)
|
||||
from allianceauth.authentication.task_statistics.helpers import _RedisStub
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
||||
|
||||
|
||||
class TestEventSeries(TestCase):
|
||||
def test_should_abort_without_redis_client(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock:
|
||||
mock.return_value = None
|
||||
events = EventSeries("dummy")
|
||||
# then
|
||||
self.assertTrue(events._redis, _RedisStub)
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
def test_should_disable_itself_if_redis_not_available_1(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.side_effect = RedisError
|
||||
events = EventSeries("dummy")
|
||||
# then
|
||||
self.assertIsInstance(events._redis, _RedisStub)
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
def test_should_disable_itself_if_redis_not_available_2(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.return_value = False
|
||||
events = EventSeries("dummy")
|
||||
# then
|
||||
self.assertIsInstance(events._redis, _RedisStub)
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
def test_should_add_event(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
@@ -166,3 +137,15 @@ class TestEventSeries(TestCase):
|
||||
results = events.all()
|
||||
# then
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
def test_should_not_report_as_disabled_when_initialized_normally(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
# when/then
|
||||
self.assertFalse(events.is_disabled)
|
||||
|
||||
def test_should_report_as_disabled_when_initialized_with_redis_stub(self):
|
||||
# given
|
||||
events = EventSeries("dummy", redis=_RedisStub())
|
||||
# when/then
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from redis import RedisError
|
||||
|
||||
from allianceauth.authentication.task_statistics.helpers import (
|
||||
ItemCounter, _RedisStub, get_redis_client_or_stub,
|
||||
)
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"
|
||||
|
||||
COUNTER_NAME = "test-counter"
|
||||
|
||||
|
||||
class TestItemCounter(TestCase):
|
||||
def test_can_create_counter(self):
|
||||
# when
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
# then
|
||||
self.assertIsInstance(counter, ItemCounter)
|
||||
|
||||
def test_can_reset_counter_to_default(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
# when
|
||||
counter.reset()
|
||||
# then
|
||||
self.assertEqual(counter.value(), 0)
|
||||
|
||||
def test_can_reset_counter_to_custom_value(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
# when
|
||||
counter.reset(42)
|
||||
# then
|
||||
self.assertEqual(counter.value(), 42)
|
||||
|
||||
def test_can_increment_counter_by_default(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(0)
|
||||
# when
|
||||
counter.incr()
|
||||
# then
|
||||
self.assertEqual(counter.value(), 1)
|
||||
|
||||
def test_can_increment_counter_by_custom_value(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(0)
|
||||
# when
|
||||
counter.incr(8)
|
||||
# then
|
||||
self.assertEqual(counter.value(), 8)
|
||||
|
||||
def test_can_decrement_counter_by_default(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(9)
|
||||
# when
|
||||
counter.decr()
|
||||
# then
|
||||
self.assertEqual(counter.value(), 8)
|
||||
|
||||
def test_can_decrement_counter_by_custom_value(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(9)
|
||||
# when
|
||||
counter.decr(8)
|
||||
# then
|
||||
self.assertEqual(counter.value(), 1)
|
||||
|
||||
def test_can_decrement_counter_below_zero(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(0)
|
||||
# when
|
||||
counter.decr(1)
|
||||
# then
|
||||
self.assertEqual(counter.value(), -1)
|
||||
|
||||
def test_can_not_decrement_counter_below_minimum(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME, minimum=0)
|
||||
counter.reset(0)
|
||||
# when
|
||||
counter.decr(1)
|
||||
# then
|
||||
self.assertEqual(counter.value(), 0)
|
||||
|
||||
def test_can_not_reset_counter_below_minimum(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME, minimum=0)
|
||||
# when/then
|
||||
with self.assertRaises(ValueError):
|
||||
counter.reset(-1)
|
||||
|
||||
def test_can_not_init_without_name(self):
|
||||
# when/then
|
||||
with self.assertRaises(ValueError):
|
||||
ItemCounter(name="")
|
||||
|
||||
def test_can_ignore_invalid_values_when_incrementing(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(0)
|
||||
# when
|
||||
with patch(MODULE_PATH + ".cache.incr") as m:
|
||||
m.side_effect = ValueError
|
||||
counter.incr()
|
||||
# then
|
||||
self.assertEqual(counter.value(), 0)
|
||||
|
||||
def test_can_ignore_invalid_values_when_decrementing(self):
|
||||
# given
|
||||
counter = ItemCounter(COUNTER_NAME)
|
||||
counter.reset(1)
|
||||
# when
|
||||
with patch(MODULE_PATH + ".cache.decr") as m:
|
||||
m.side_effect = ValueError
|
||||
counter.decr()
|
||||
# then
|
||||
self.assertEqual(counter.value(), 1)
|
||||
|
||||
|
||||
class TestGetRedisClient(TestCase):
|
||||
def test_should_return_mock_if_redis_not_available_1(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.side_effect = RedisError
|
||||
result = get_redis_client_or_stub()
|
||||
# then
|
||||
self.assertIsInstance(result, _RedisStub)
|
||||
|
||||
def test_should_return_mock_if_redis_not_available_2(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.return_value = False
|
||||
result = get_redis_client_or_stub()
|
||||
# then
|
||||
self.assertIsInstance(result, _RedisStub)
|
||||
@@ -17,16 +17,17 @@ from allianceauth.eveonline.tasks import update_character
|
||||
|
||||
|
||||
@override_settings(
|
||||
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||
CELERY_ALWAYS_EAGER=True, ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||
)
|
||||
class TestTaskSignals(TestCase):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def test_should_record_successful_task(self):
|
||||
# given
|
||||
def setUp(self) -> None:
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
|
||||
def test_should_record_successful_task(self):
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
@@ -39,10 +40,6 @@ class TestTaskSignals(TestCase):
|
||||
self.assertEqual(failed_tasks.count(), 0)
|
||||
|
||||
def test_should_record_retried_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
@@ -55,10 +52,6 @@ class TestTaskSignals(TestCase):
|
||||
self.assertEqual(retried_tasks.count(), 1)
|
||||
|
||||
def test_should_record_failed_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<div class="panel panel-primary" style="height:100%">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% blocktrans with state=request.user.profile.state %}
|
||||
{% blocktranslate with state=request.user.profile.state %}
|
||||
Main Character (State: {{ state }})
|
||||
{% endblocktrans %}
|
||||
{% endblocktranslate %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
@@ -103,13 +103,17 @@
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 button-wrapper">
|
||||
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
||||
title="Add Character">{% translate 'Add Character' %}</a>
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
||||
title="Add Character">{% translate 'Add Character' %}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6 button-wrapper">
|
||||
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
|
||||
title="Change Main Character">{% translate "Change Main" %}</a>
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
|
||||
title="Change Main Character">{% translate "Change Main" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +163,7 @@
|
||||
</td>
|
||||
<td class="text-center">{{ char.character_name }}</td>
|
||||
<td class="text-center">{{ char.corporation_name }}</td>
|
||||
<td class="text-center">{{ char.alliance_name }}</td>
|
||||
<td class="text-center">{{ char.alliance_name|default:"" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="page-header text-center">{% translate "Token Management" %}</h1>
|
||||
<div class="col-sm-12">
|
||||
<table class="table table-aa" id="table_tokens" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Scopes" %}</th>
|
||||
<th class="text-right">{% translate "Actions" %}</th>
|
||||
<th>{% translate "Character" %}</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tokens %}
|
||||
<tr>
|
||||
<td styl="white-space:initial;">{% for s in t.scopes.all %}<span class="label label-default">{{s.name}}</span> {% endfor %}</td>
|
||||
<td nowrap class="text-right"><a href="{% url 'authentication:token_delete' t.id %}" class="btn btn-danger"><i class="fas fa-trash"></i></a> <a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fas fa-sync-alt"></i></a></td>
|
||||
<td>{{t.character_name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
$(document).ready(function(){
|
||||
let grp = 2;
|
||||
var table = $('#table_tokens').DataTable({
|
||||
"columnDefs": [{ orderable: false, targets: [0,1] },{ "visible": false, "targets": grp }],
|
||||
"order": [[grp, 'asc']],
|
||||
"drawCallback": function (settings) {
|
||||
var api = this.api();
|
||||
var rows = api.rows({ page: 'current' }).nodes();
|
||||
var last = null;
|
||||
api.column(grp, { page: 'current' })
|
||||
.data()
|
||||
.each(function (group, i) {
|
||||
if (last !== group) {
|
||||
$(rows).eq(i).before('<tr class="info"><td colspan="3">' + group + '</td></tr>');
|
||||
last = group;
|
||||
}
|
||||
});
|
||||
},
|
||||
"stateSave": true,
|
||||
});
|
||||
});
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -7,7 +8,7 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta property="og:title" content="{{ SITE_NAME }}">
|
||||
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
|
||||
|
||||
{% include 'allianceauth/icons.html' %}
|
||||
@@ -31,6 +32,7 @@
|
||||
.panel-transparent {
|
||||
background: rgba(48, 48, 48, 0.7);
|
||||
color: #ffffff;
|
||||
padding-bottom: 21px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
{{ language.name_local }} ({{ language.code }})
|
||||
{{ language.name_local|capfirst }} ({{ language.code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% block page_title %}{% translate "Login" %}{% endblock %}
|
||||
|
||||
{% block middle_box_content %}
|
||||
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}">
|
||||
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next | urlencode}}{%endif%}">
|
||||
<img class="img-responsive center-block" src="{% static 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" alt="{% translate 'Login with Eve SSO' %}">
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{% extends 'public/base.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
{% if messages %}
|
||||
@@ -6,6 +9,7 @@
|
||||
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="panel panel-default panel-transparent">
|
||||
<div class="panel-body">
|
||||
<div class="col-md-12">
|
||||
@@ -13,10 +17,25 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'public/lang_select.html' %}
|
||||
|
||||
<p class="text-center" style="margin-top: 2rem;">
|
||||
{% translate "For information on SSO, ESI and security read the CCP Dev Blog" %}<br>
|
||||
<a href="https://www.eveonline.com/article/introducing-esi" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Introducing ESI - A New API For Eve Online" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
<a href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Manage ESI Applications" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_include %}
|
||||
{% include 'bundles/bootstrap-js.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -116,10 +116,17 @@ class TestAuthenticate(TestCase):
|
||||
user = StateBackend().authenticate(token=t)
|
||||
self.assertEqual(user, self.user)
|
||||
|
||||
""" Alt Login disabled
|
||||
def test_authenticate_alt_character(self):
|
||||
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
|
||||
user = StateBackend().authenticate(token=t)
|
||||
self.assertEqual(user, self.user)
|
||||
"""
|
||||
|
||||
def test_authenticate_alt_character_fail(self):
|
||||
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
|
||||
user = StateBackend().authenticate(token=t)
|
||||
self.assertEqual(user, None)
|
||||
|
||||
def test_authenticate_unclaimed_character(self):
|
||||
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
|
||||
@@ -128,6 +135,7 @@ class TestAuthenticate(TestCase):
|
||||
self.assertEqual(user.username, 'Unclaimed_Character')
|
||||
self.assertEqual(user.profile.main_character, self.unclaimed_character)
|
||||
|
||||
""" Alt Login disabled
|
||||
def test_authenticate_character_record(self):
|
||||
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
|
||||
OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
|
||||
@@ -135,6 +143,15 @@ class TestAuthenticate(TestCase):
|
||||
self.assertEqual(user, self.old_user)
|
||||
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
||||
self.assertTrue(user.profile.main_character)
|
||||
"""
|
||||
|
||||
def test_authenticate_character_record_fails(self):
|
||||
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
|
||||
OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
|
||||
user = StateBackend().authenticate(token=t)
|
||||
self.assertEqual(user, self.old_user)
|
||||
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
||||
self.assertTrue(user.profile.main_character)
|
||||
|
||||
def test_iterate_username(self):
|
||||
t = Token(character_id=self.unclaimed_character.character_id,
|
||||
|
||||
@@ -4,16 +4,16 @@ from urllib import parse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http.response import HttpResponse
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse, URLPattern
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..decorators import main_character_required
|
||||
from ..models import CharacterOwnership
|
||||
|
||||
from ..decorators import decorate_url_patterns, main_character_required
|
||||
from ..models import CharacterOwnership
|
||||
|
||||
MODULE_PATH = 'allianceauth.authentication'
|
||||
|
||||
@@ -66,3 +66,33 @@ class DecoratorTestCase(TestCase):
|
||||
setattr(self.request, 'user', self.main_user)
|
||||
response = self.dummy_view(self.request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestDecorateUrlPatterns(TestCase):
|
||||
def test_should_add_decorator_by_default(self):
|
||||
# given
|
||||
decorator = mock.MagicMock(name="decorator")
|
||||
view = mock.MagicMock(name="view")
|
||||
path = mock.MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
path.lookup_str = "my_lookup_str"
|
||||
urls = [path]
|
||||
urlconf_module = urls
|
||||
# when
|
||||
decorate_url_patterns(urlconf_module, decorator)
|
||||
# then
|
||||
self.assertEqual(path.callback, decorator(view))
|
||||
|
||||
def test_should_not_add_decorator_when_excluded(self):
|
||||
# given
|
||||
decorator = mock.MagicMock(name="decorator")
|
||||
view = mock.MagicMock(name="view")
|
||||
path = mock.MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
path.lookup_str = "my_lookup_str"
|
||||
urls = [path]
|
||||
urlconf_module = urls
|
||||
# when
|
||||
decorate_url_patterns(urlconf_module, decorator, excluded_views=["my_lookup_str"])
|
||||
# then
|
||||
self.assertEqual(path.callback, view)
|
||||
|
||||
@@ -22,5 +22,20 @@ urlpatterns = [
|
||||
views.add_character,
|
||||
name='add_character'
|
||||
),
|
||||
path(
|
||||
'account/tokens/manage/',
|
||||
views.token_management,
|
||||
name='token_management'
|
||||
),
|
||||
path(
|
||||
'account/tokens/delete/<int:token_id>',
|
||||
views.token_delete,
|
||||
name='token_delete'
|
||||
),
|
||||
path(
|
||||
'account/tokens/refresh/<int:token_id>',
|
||||
views.token_refresh,
|
||||
name='token_refresh'
|
||||
),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
]
|
||||
|
||||
@@ -61,6 +61,44 @@ def dashboard(request):
|
||||
}
|
||||
return render(request, 'authentication/dashboard.html', context)
|
||||
|
||||
@login_required
|
||||
def token_management(request):
|
||||
tokens = request.user.token_set.all()
|
||||
|
||||
context = {
|
||||
'tokens': tokens
|
||||
}
|
||||
return render(request, 'authentication/tokens.html', context)
|
||||
|
||||
@login_required
|
||||
def token_delete(request, token_id=None):
|
||||
try:
|
||||
token = Token.objects.get(id=token_id)
|
||||
if request.user == token.user:
|
||||
token.delete()
|
||||
messages.success(request, "Token Deleted.")
|
||||
else:
|
||||
messages.error(request, "This token does not belong to you.")
|
||||
except Token.DoesNotExist:
|
||||
messages.warning(request, "Token does not exist")
|
||||
return redirect('authentication:token_management')
|
||||
|
||||
@login_required
|
||||
def token_refresh(request, token_id=None):
|
||||
try:
|
||||
token = Token.objects.get(id=token_id)
|
||||
if request.user == token.user:
|
||||
try:
|
||||
token.refresh()
|
||||
messages.success(request, "Token refreshed.")
|
||||
except Exception as e:
|
||||
messages.warning(request, f"Failed to refresh token. {e}")
|
||||
else:
|
||||
messages.error(request, "This token does not belong to you.")
|
||||
except Token.DoesNotExist:
|
||||
messages.warning(request, "Token does not exist")
|
||||
return redirect('authentication:token_management')
|
||||
|
||||
|
||||
@login_required
|
||||
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
||||
|
||||
@@ -5,5 +5,6 @@ from .views import NightModeRedirectView
|
||||
def auth_settings(request):
|
||||
return {
|
||||
'SITE_NAME': settings.SITE_NAME,
|
||||
'SITE_URL': settings.SITE_URL,
|
||||
'NIGHT_MODE': NightModeRedirectView.night_mode_state(request),
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ def sync_user_groups(modeladmin, request, queryset):
|
||||
agc.update_all_states_group_membership()
|
||||
|
||||
|
||||
@admin.register(AutogroupsConfig)
|
||||
class AutogroupsConfigAdmin(admin.ModelAdmin):
|
||||
formfield_overrides = {
|
||||
models.CharField: {'strip': False}
|
||||
@@ -36,6 +37,5 @@ class AutogroupsConfigAdmin(admin.ModelAdmin):
|
||||
return actions
|
||||
|
||||
|
||||
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)
|
||||
admin.site.register(ManagedCorpGroup)
|
||||
admin.site.register(ManagedAllianceGroup)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.0.7 on 2022-08-14 16:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eveonline', '0016_character_names_are_not_unique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eveallianceinfo',
|
||||
name='alliance_name',
|
||||
field=models.CharField(max_length=254, db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='evecorporationinfo',
|
||||
name='corporation_name',
|
||||
field=models.CharField(max_length=254, db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -71,7 +71,7 @@ class EveAllianceInfo(models.Model):
|
||||
"""An alliance in Eve Online."""
|
||||
|
||||
alliance_id = models.PositiveIntegerField(unique=True)
|
||||
alliance_name = models.CharField(max_length=254, unique=True)
|
||||
alliance_name = models.CharField(max_length=254, db_index=True)
|
||||
alliance_ticker = models.CharField(max_length=254)
|
||||
executor_corp_id = models.PositiveIntegerField()
|
||||
|
||||
@@ -139,7 +139,7 @@ class EveCorporationInfo(models.Model):
|
||||
"""A corporation in Eve Online."""
|
||||
|
||||
corporation_id = models.PositiveIntegerField(unique=True)
|
||||
corporation_name = models.CharField(max_length=254, unique=True)
|
||||
corporation_name = models.CharField(max_length=254, db_index=True)
|
||||
corporation_ticker = models.CharField(max_length=254)
|
||||
member_count = models.IntegerField()
|
||||
ceo_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.conf import settings
|
||||
from esi.clients import esi_client_factory
|
||||
|
||||
from allianceauth import __version__
|
||||
from allianceauth.utils.django import StartupCommand
|
||||
|
||||
|
||||
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
|
||||
@@ -175,15 +176,16 @@ class EveProvider:
|
||||
|
||||
class EveSwaggerProvider(EveProvider):
|
||||
def __init__(self, token=None, adapter=None):
|
||||
if settings.DEBUG:
|
||||
if settings.DEBUG or StartupCommand().is_management_command:
|
||||
self._client = None
|
||||
logger.info(
|
||||
'DEBUG mode detected: ESI client will be loaded on-demand.'
|
||||
)
|
||||
logger.info('ESI client will be loaded on-demand')
|
||||
else:
|
||||
logger.info('Loading ESI client')
|
||||
try:
|
||||
self._client = esi_client_factory(
|
||||
token=token, spec_file=SWAGGER_SPEC_PATH, app_info_text=("allianceauth v" + __version__)
|
||||
token=token,
|
||||
spec_file=SWAGGER_SPEC_PATH,
|
||||
app_info_text=f"allianceauth v{__version__}"
|
||||
)
|
||||
except (HTTPError, RefResolutionError):
|
||||
logger.exception(
|
||||
|
||||
0
allianceauth/eveonline/views.py
Executable file → Normal file
0
allianceauth/eveonline/views.py
Executable file → Normal file
@@ -30,7 +30,7 @@
|
||||
<td class="text-center">{{ fat.user }}</td>
|
||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||
{% if fat.station != "No Station" %}
|
||||
<td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
|
||||
<td class="text-center">{% blocktranslate %}Docked in {% endblocktranslate %}{{ fat.system }}</td>
|
||||
{% else %}
|
||||
<td class="text-center">{{ fat.system }}</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
{% if char_id %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
@@ -14,11 +14,11 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
<h2>
|
||||
{% blocktrans count links=n_fats trimmed %}
|
||||
{% blocktranslate count links=n_fats trimmed %}
|
||||
{{ user }} has collected one link this month.
|
||||
{% plural %}
|
||||
{{ user }} has collected {{ links }} links this month.
|
||||
{% endblocktrans %}
|
||||
{% endblocktranslate %}
|
||||
</h2>
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
@@ -34,11 +34,11 @@
|
||||
</table>
|
||||
{% if created_fats %}
|
||||
<h2>
|
||||
{% blocktrans count links=n_created_fats trimmed %}
|
||||
{% blocktranslate count links=n_created_fats trimmed %}
|
||||
{{ user }} has created one link this month.
|
||||
{% plural %}
|
||||
{{ user }} has created {{ links }} links this month.
|
||||
{% endblocktrans %}
|
||||
{% endblocktranslate %}
|
||||
</h2>
|
||||
{% if created_fats %}
|
||||
<table class="table">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ year }}{% endblocktrans %}
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ year }}{% endblocktranslate %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:personal_statistics_year' previous_year %}" class="btn btn-info">{% translate "Previous year" %}</a>
|
||||
{% if next_year %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:statistics_corp_month' corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
{% if next_month %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:statistics_month' previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
{% if next_month %}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<td class="text-center">{{ fat.fatlink.fleet }}</td>
|
||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||
{% if fat.station != "No Station" %}
|
||||
<td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
|
||||
<td class="text-center">{% blocktranslate %}Docked in {% endblocktranslate %}{{ fat.system }}</td>
|
||||
{% else %}
|
||||
<td class="text-center">{{ fat.system }}</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -248,59 +248,82 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
|
||||
|
||||
@login_required
|
||||
@token_required(
|
||||
scopes=['esi-location.read_location.v1', 'esi-location.read_ship_type.v1', 'esi-universe.read_structures.v1'])
|
||||
scopes=[
|
||||
'esi-location.read_location.v1',
|
||||
'esi-location.read_ship_type.v1',
|
||||
'esi-universe.read_structures.v1',
|
||||
'esi-location.read_online.v1',
|
||||
]
|
||||
)
|
||||
def click_fatlink_view(request, token, fat_hash=None):
|
||||
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
|
||||
c = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
||||
character = EveCharacter.objects.get_character_by_id(token.character_id)
|
||||
character_online = c.Location.get_characters_character_id_online(
|
||||
character_id=token.character_id
|
||||
).result()
|
||||
|
||||
if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
|
||||
if character_online["online"] is True:
|
||||
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
|
||||
|
||||
character = EveCharacter.objects.get_character_by_id(token.character_id)
|
||||
if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
|
||||
if character:
|
||||
# get data
|
||||
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
|
||||
ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result()
|
||||
location['solar_system_name'] = \
|
||||
c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()['name']
|
||||
|
||||
if character:
|
||||
# get data
|
||||
c = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
||||
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
|
||||
ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result()
|
||||
location['solar_system_name'] = \
|
||||
c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()['name']
|
||||
if location['station_id']:
|
||||
location['station_name'] = \
|
||||
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
|
||||
elif location['structure_id']:
|
||||
location['station_name'] = \
|
||||
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
|
||||
'name']
|
||||
if location['station_id']:
|
||||
location['station_name'] = \
|
||||
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
|
||||
elif location['structure_id']:
|
||||
location['station_name'] = \
|
||||
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
|
||||
'name']
|
||||
else:
|
||||
location['station_name'] = "No Station"
|
||||
|
||||
ship['ship_type_name'] = provider.get_itemtype(ship['ship_type_id']).name
|
||||
|
||||
fat = Fat()
|
||||
fat.system = location['solar_system_name']
|
||||
fat.station = location['station_name']
|
||||
fat.shiptype = ship['ship_type_name']
|
||||
fat.fatlink = fatlink
|
||||
fat.character = character
|
||||
fat.user = request.user
|
||||
|
||||
try:
|
||||
fat.full_clean()
|
||||
fat.save()
|
||||
messages.success(request, _('Fleet participation registered.'))
|
||||
except ValidationError as e:
|
||||
err_messages = []
|
||||
|
||||
for errorname, message in e.message_dict.items():
|
||||
err_messages.append(message[0])
|
||||
|
||||
messages.error(request, ' '.join(err_messages))
|
||||
else:
|
||||
location['station_name'] = "No Station"
|
||||
ship['ship_type_name'] = provider.get_itemtype(ship['ship_type_id']).name
|
||||
context = {
|
||||
'character_id': token.character_id,
|
||||
'character_name': token.character_name,
|
||||
'character_portrait_url': EveCharacter.generic_portrait_url(
|
||||
token.character_id, 128
|
||||
),
|
||||
}
|
||||
|
||||
fat = Fat()
|
||||
fat.system = location['solar_system_name']
|
||||
fat.station = location['station_name']
|
||||
fat.shiptype = ship['ship_type_name']
|
||||
fat.fatlink = fatlink
|
||||
fat.character = character
|
||||
fat.user = request.user
|
||||
try:
|
||||
fat.full_clean()
|
||||
fat.save()
|
||||
messages.success(request, _('Fleet participation registered.'))
|
||||
except ValidationError as e:
|
||||
err_messages = []
|
||||
for errorname, message in e.message_dict.items():
|
||||
err_messages.append(message[0])
|
||||
messages.error(request, ' '.join(err_messages))
|
||||
return render(request, 'fleetactivitytracking/characternotexisting.html', context=context)
|
||||
else:
|
||||
context = {
|
||||
'character_id': token.character_id,
|
||||
'character_name': token.character_name,
|
||||
'character_portrait_url': EveCharacter.generic_portrait_url(
|
||||
token.character_id, 128
|
||||
),
|
||||
}
|
||||
return render(request, 'fleetactivitytracking/characternotexisting.html', context=context)
|
||||
messages.error(request, _('FAT link has expired.'))
|
||||
else:
|
||||
messages.error(request, _('FAT link has expired.'))
|
||||
messages.warning(
|
||||
request,
|
||||
_(
|
||||
f"Cannot register the fleet participation for {character.character_name}. The character needs to be online."
|
||||
),
|
||||
)
|
||||
|
||||
return redirect('fatlink:view')
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{{ group }} {% translate "Audit Log" %}{% endblock page_title %}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<i class="glyphicon glyphicon-list-alt"></i>
|
||||
</a>
|
||||
|
||||
<a id="clipboard-copy" data-clipboard-text="{{ request.scheme }}://{{request.get_host}}{% url 'groupmanagement:request_add' group.id %}" class="btn btn-warning" title="{% translate "Copy Direct Join Link" %}">
|
||||
<a id="clipboard-copy" data-clipboard-text="{{ SITE_URL }}{% url 'groupmanagement:request_add' group.id %}" class="btn btn-warning" title="{% translate "Copy Direct Join Link" %}">
|
||||
<i class="glyphicon glyphicon-copy"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -5,7 +5,7 @@ app_name = "groupmanagement"
|
||||
|
||||
urlpatterns = [
|
||||
# groups
|
||||
path("groups", views.groups_view, name="groups"),
|
||||
path("groups/", views.groups_view, name="groups"),
|
||||
path("group/request/join/<int:group_id>/", views.group_request_add, name="request_add"),
|
||||
path(
|
||||
"group/request/leave/<int:group_id>/", views.group_request_leave, name="request_leave"
|
||||
|
||||
0
allianceauth/groupmanagement/views.py
Executable file → Normal file
0
allianceauth/groupmanagement/views.py
Executable file → Normal file
2
allianceauth/hrapplications/admin.py
Executable file → Normal file
2
allianceauth/hrapplications/admin.py
Executable file → Normal file
@@ -10,6 +10,7 @@ class ChoiceInline(admin.TabularInline):
|
||||
verbose_name_plural = 'Choices (optional)'
|
||||
verbose_name= 'Choice'
|
||||
|
||||
@admin.register(ApplicationQuestion)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['title', 'help_text', 'multi_select']}),
|
||||
@@ -18,6 +19,5 @@ class QuestionAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(Application)
|
||||
admin.site.register(ApplicationComment)
|
||||
admin.site.register(ApplicationQuestion, QuestionAdmin)
|
||||
admin.site.register(ApplicationForm)
|
||||
admin.site.register(ApplicationResponse)
|
||||
|
||||
0
allianceauth/hrapplications/forms.py
Executable file → Normal file
0
allianceauth/hrapplications/forms.py
Executable file → Normal file
0
allianceauth/hrapplications/models.py
Executable file → Normal file
0
allianceauth/hrapplications/models.py
Executable file → Normal file
18
allianceauth/hrapplications/views.py
Executable file → Normal file
18
allianceauth/hrapplications/views.py
Executable file → Normal file
@@ -57,7 +57,7 @@ def hr_application_create_view(request, form_id=None):
|
||||
app_form = get_object_or_404(ApplicationForm, id=form_id)
|
||||
if request.method == "POST":
|
||||
if Application.objects.filter(user=request.user).filter(form=app_form).exists():
|
||||
logger.warn(f"User {request.user} attempting to duplicate application to {app_form.corp}")
|
||||
logger.warning(f"User {request.user} attempting to duplicate application to {app_form.corp}")
|
||||
else:
|
||||
application = Application(user=request.user, form=app_form)
|
||||
application.save()
|
||||
@@ -92,7 +92,7 @@ def hr_application_personal_view(request, app_id):
|
||||
}
|
||||
return render(request, 'hrapplications/view.html', context=context)
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to view {app}")
|
||||
logger.warning(f"User {request.user} not authorized to view {app}")
|
||||
return redirect('hrapplications:personal_view')
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ def hr_application_personal_removal(request, app_id):
|
||||
logger.info(f"User {request.user} deleting {app}")
|
||||
app.delete()
|
||||
else:
|
||||
logger.warn(f"User {request.user} attempting to delete reviewed app {app}")
|
||||
logger.warning(f"User {request.user} attempting to delete reviewed app {app}")
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to delete {app}")
|
||||
logger.warning(f"User {request.user} not authorized to delete {app}")
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ def hr_application_view(request, app_id):
|
||||
logger.info(f"Saved comment by user {request.user} to {app}")
|
||||
return redirect('hrapplications:view', app_id)
|
||||
else:
|
||||
logger.warn("User %s does not have permission to add ApplicationComments" % request.user)
|
||||
logger.warning("User %s does not have permission to add ApplicationComments" % request.user)
|
||||
return redirect('hrapplications:view', app_id)
|
||||
else:
|
||||
logger.debug("Returning blank HRApplication comment form.")
|
||||
@@ -171,7 +171,7 @@ def hr_application_approve(request, app_id):
|
||||
app.save()
|
||||
notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success")
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to approve {app}")
|
||||
logger.warning(f"User {request.user} not authorized to approve {app}")
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ def hr_application_reject(request, app_id):
|
||||
app.save()
|
||||
notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger")
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to reject {app}")
|
||||
logger.warning(f"User {request.user} not authorized to reject {app}")
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ def hr_application_search(request):
|
||||
app_list = app_list.filter(
|
||||
form__corp__corporation_id=request.user.profile.main_character.corporation_id)
|
||||
except AttributeError:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
"User %s missing main character model: unable to filter applications to search" % request.user)
|
||||
|
||||
applications = app_list.filter(
|
||||
@@ -246,6 +246,6 @@ def hr_application_mark_in_progress(request, app_id):
|
||||
app.save()
|
||||
notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}")
|
||||
else:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}")
|
||||
return redirect("hrapplications:view", app_id)
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
allianceauth/locale/uk/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/uk/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2410
allianceauth/locale/uk/LC_MESSAGES/django.po
Normal file
2410
allianceauth/locale/uk/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1 @@
|
||||
from .core import notify # noqa: F401
|
||||
|
||||
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
|
||||
|
||||
@@ -15,18 +15,22 @@ class NotificationAdmin(admin.ModelAdmin):
|
||||
ordering = ("-timestamp", )
|
||||
search_fields = ["user__username", "user__profile__main_character__character_name"]
|
||||
|
||||
@admin.display(
|
||||
ordering="user__profile__main_character__character_name"
|
||||
)
|
||||
def _main(self, obj):
|
||||
try:
|
||||
return obj.user.profile.main_character
|
||||
except AttributeError:
|
||||
return obj.user
|
||||
|
||||
_main.admin_order_field = "user__profile__main_character__character_name"
|
||||
|
||||
@admin.display(
|
||||
ordering="user__profile__state__name"
|
||||
)
|
||||
def _state(self, obj):
|
||||
return obj.user.profile.state
|
||||
|
||||
_state.admin_order_field = "user__profile__state__name"
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@@ -44,7 +44,7 @@ def notification_view(request, notif_id):
|
||||
notif.mark_viewed()
|
||||
return render(request, 'notifications/view.html', context)
|
||||
else:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
"User %s not authorized to view notif_id %s belonging to user %s",
|
||||
request.user,
|
||||
notif_id, notif.user
|
||||
|
||||
@@ -34,7 +34,7 @@ class OpTimer(models.Model):
|
||||
fc = models.CharField(max_length=254, default="")
|
||||
post_time = models.DateTimeField(default=timezone.now)
|
||||
eve_character = models.ForeignKey(EveCharacter, null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
on_delete=models.SET_NULL)
|
||||
description = models.TextField(blank=True, default="")
|
||||
type = models.ForeignKey(OpTimerType, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<h1 class="page-header">{% translate "Permissions Overview" %}</h1>
|
||||
<p>
|
||||
{% if request.GET.all != 'yes' %}
|
||||
{% blocktrans %}Showing only applied permissions{% endblocktrans %}
|
||||
{% blocktranslate %}Showing only applied permissions{% endblocktranslate %}
|
||||
<a href="{% url 'permissions_tool:overview' %}?all=yes" class="btn btn-primary">{% translate "Show All" %}</a>
|
||||
{% else %}
|
||||
{% blocktrans %}Showing all permissions{% endblocktrans %}
|
||||
{% blocktranslate %}Showing all permissions{% endblocktranslate %}
|
||||
<a href="{% url 'permissions_tool:overview' %}?all=no" class="btn btn-primary">{% translate "Show Applied" %}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
@@ -41,23 +41,23 @@ CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
|
||||
CELERYBEAT_SCHEDULE = {
|
||||
'esi_cleanup_callbackredirect': {
|
||||
'task': 'esi.tasks.cleanup_callbackredirect',
|
||||
'schedule': crontab(minute=0, hour='*/4'),
|
||||
'schedule': crontab(minute='0', hour='*/4'),
|
||||
},
|
||||
'esi_cleanup_token': {
|
||||
'task': 'esi.tasks.cleanup_token',
|
||||
'schedule': crontab(minute=0, hour=0),
|
||||
'schedule': crontab(minute='0', hour='0'),
|
||||
},
|
||||
'run_model_update': {
|
||||
'task': 'allianceauth.eveonline.tasks.run_model_update',
|
||||
'schedule': crontab(minute=0, hour="*/6"),
|
||||
'schedule': crontab(minute='0', hour="*/6"),
|
||||
},
|
||||
'check_all_character_ownership': {
|
||||
'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
|
||||
'schedule': crontab(minute=0, hour='*/4'),
|
||||
'schedule': crontab(minute='0', hour='*/4'),
|
||||
},
|
||||
'analytics_daily_stats': {
|
||||
'task': 'allianceauth.analytics.tasks.analytics_daily_stats',
|
||||
'schedule': crontab(minute=0, hour=2),
|
||||
'schedule': crontab(minute='0', hour='2'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,17 +84,17 @@ LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale/'),
|
||||
)
|
||||
|
||||
ugettext = lambda s: s
|
||||
LANGUAGES = (
|
||||
('en', ugettext('English')),
|
||||
('de', ugettext('German')),
|
||||
('es', ugettext('Spanish')),
|
||||
('zh-hans', ugettext('Chinese Simplified')),
|
||||
('ru', ugettext('Russian')),
|
||||
('ko', ugettext('Korean')),
|
||||
('fr', ugettext('French')),
|
||||
('ja', ugettext('Japanese')),
|
||||
('it', ugettext('Italian')),
|
||||
("en", "English"),
|
||||
("de", "German"),
|
||||
("es", "Spanish"),
|
||||
("zh-hans", "Chinese Simplified"),
|
||||
("ru", "Russian"),
|
||||
("ko", "Korean"),
|
||||
("fr", "French"),
|
||||
("ja", "Japanese"),
|
||||
("it", "Italian"),
|
||||
("uk", "Ukrainian"),
|
||||
)
|
||||
|
||||
TEMPLATES = [
|
||||
|
||||
@@ -13,6 +13,13 @@ STATIC_ROOT = "/var/www/{{ project_name }}/static/"
|
||||
# in page titles and the site header.
|
||||
SITE_NAME = '{{ project_name }}'
|
||||
|
||||
# This is your websites URL, set it accordingly
|
||||
# Make sure this URL is WITHOUT a trailing slash
|
||||
SITE_URL = "https://example.com"
|
||||
|
||||
# Django security
|
||||
CSRF_TRUSTED_ORIGINS = [SITE_URL]
|
||||
|
||||
# Change this to enable/disable debug mode, which displays
|
||||
# useful error messages but can leak sensitive data.
|
||||
DEBUG = False
|
||||
@@ -25,6 +32,13 @@ INSTALLED_APPS += [
|
||||
# To change the logging level for extensions, uncomment the following line.
|
||||
# LOGGING['handlers']['extension_file']['level'] = 'DEBUG'
|
||||
|
||||
# By default, apps are prevented from having public views for security reasons.
|
||||
# To allow specific apps to have public views, add them to APPS_WITH_PUBLIC_VIEWS
|
||||
# » The format is the same as in INSTALLED_APPS
|
||||
# » The app developer must also explicitly allow public views for their app
|
||||
APPS_WITH_PUBLIC_VIEWS = [
|
||||
|
||||
]
|
||||
|
||||
# Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
|
||||
DATABASES['default'] = {
|
||||
@@ -39,15 +53,16 @@ DATABASES['default'] = {
|
||||
|
||||
# Register an application at https://developers.eveonline.com for Authentication
|
||||
# & API Access and fill out these settings. Be sure to set the callback URL
|
||||
# to https://example.com/sso/callback substituting your domain for example.com
|
||||
# to https://example.com/sso/callback substituting your domain for example.com in
|
||||
# CCP's developer portal
|
||||
# Logging in to auth requires the publicData scope (can be overridden through the
|
||||
# LOGIN_TOKEN_SCOPES setting). Other apps may require more (see their docs).
|
||||
ESI_SSO_CLIENT_ID = ''
|
||||
ESI_SSO_CLIENT_SECRET = ''
|
||||
ESI_SSO_CALLBACK_URL = ''
|
||||
ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback"
|
||||
ESI_USER_CONTACT_EMAIL = '' # A server maintainer that CCP can contact in case of issues.
|
||||
|
||||
# By default emails are validated before new users can log in.
|
||||
# By default, emails are validated before new users can log in.
|
||||
# It's recommended to use a free service like SparkPost or Elastic Email to send email.
|
||||
# https://www.sparkpost.com/docs/integrations/django/
|
||||
# https://elasticemail.com/resources/settings/smtp-api/
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.conf.urls import include
|
||||
from allianceauth import urls
|
||||
from django.urls import re_path
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'', include(urls)),
|
||||
path('', include(urls)),
|
||||
]
|
||||
|
||||
handler500 = 'allianceauth.views.Generic500Redirect'
|
||||
|
||||
@@ -66,6 +66,8 @@ class NameFormatConfigAdmin(admin.ModelAdmin):
|
||||
form = NameFormatConfigForm
|
||||
list_display = ('service_name', 'get_state_display_string')
|
||||
|
||||
@admin.display(
|
||||
description='States'
|
||||
)
|
||||
def get_state_display_string(self, obj):
|
||||
return ', '.join([state.name for state in obj.states.all()])
|
||||
get_state_display_string.short_description = 'States'
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import re_path
|
||||
from string import Formatter
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import include, re_path
|
||||
from django.utils.functional import cached_property
|
||||
from django.conf import settings
|
||||
from string import Formatter
|
||||
|
||||
from allianceauth.hooks import get_hooks
|
||||
|
||||
from .models import NameFormatConfig
|
||||
|
||||
|
||||
def get_extension_logger(name):
|
||||
"""
|
||||
Takes the name of a plugin/extension and generates a child logger of the extensions logger
|
||||
@@ -156,8 +158,32 @@ class MenuItemHook:
|
||||
|
||||
|
||||
class UrlHook:
|
||||
def __init__(self, urls, namespace, base_url):
|
||||
"""A hook for registering the URLs of a Django app.
|
||||
|
||||
Args:
|
||||
- urls: The urls module to include
|
||||
- namespace: The URL namespace to apply. This is usually just the app name.
|
||||
- base_url: The URL prefix to match against in regex form.
|
||||
Example ``r'^app_name/'``.
|
||||
This prefix will be applied in front of all URL patterns included.
|
||||
It is possible to use the same prefix as existing apps (or no prefix at all),
|
||||
but standard URL resolution ordering applies
|
||||
(hook URLs are the last ones registered).
|
||||
- excluded_views: Optional list of views to be excluded
|
||||
from auto-decorating them with the
|
||||
default ``main_character_required`` decorator, e.g. to make them public.
|
||||
Views must be specified by their qualified name,
|
||||
e.g. ``["example.views.my_public_view"]``
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
urls,
|
||||
namespace: str,
|
||||
base_url: str,
|
||||
excluded_views : Optional[Iterable[str]] = None
|
||||
):
|
||||
self.include_pattern = re_path(base_url, include(urls, namespace=namespace))
|
||||
self.excluded_views = set(excluded_views or [])
|
||||
|
||||
|
||||
class NameFormatter:
|
||||
|
||||
@@ -588,16 +588,17 @@ class DiscordClient:
|
||||
return None # User is no longer a member
|
||||
guild_roles = RolesSet(self.guild_roles(guild_id=guild_id))
|
||||
logger.debug('Current guild roles: %s', guild_roles.ids())
|
||||
_roles = set(member_info.roles)
|
||||
if not guild_roles.has_roles(member_info.roles):
|
||||
guild_roles = RolesSet(
|
||||
self.guild_roles(guild_id=guild_id, use_cache=False)
|
||||
)
|
||||
if not guild_roles.has_roles(member_info.roles):
|
||||
role_ids = set(member_info.roles).difference(guild_roles.ids())
|
||||
raise RuntimeError(
|
||||
f'Discord user {user_id} has unknown roles: {role_ids}'
|
||||
)
|
||||
return guild_roles.subset(member_info.roles)
|
||||
logger.warning(f'Discord user {user_id} has unknown roles: {role_ids}')
|
||||
for _r in role_ids:
|
||||
_roles.remove(_r)
|
||||
return guild_roles.subset(_roles)
|
||||
|
||||
@classmethod
|
||||
def _is_member_unknown_error(cls, r: requests.Response) -> bool:
|
||||
|
||||
@@ -899,8 +899,8 @@ class TestGuildMemberRoles(NoSocketsTestCase):
|
||||
mock_guild_roles.return_value = {role_a, role_b}
|
||||
client = DiscordClientStub(TEST_BOT_TOKEN, mock_redis)
|
||||
# when/then
|
||||
with self.assertRaises(RuntimeError):
|
||||
client.guild_member_roles(TEST_GUILD_ID, TEST_USER_ID)
|
||||
roles = client.guild_member_roles(TEST_GUILD_ID, TEST_USER_ID)
|
||||
self.assertEqual(roles, RolesSet([role_a]))
|
||||
|
||||
# TODO: Re-enable after adding Discord general error handling
|
||||
# def test_should_raise_exception_if_member_info_is_invalid(
|
||||
|
||||
0
allianceauth/services/modules/discord/tests/piloting_tasks.py
Executable file → Normal file
0
allianceauth/services/modules/discord/tests/piloting_tasks.py
Executable file → Normal file
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class DiscourseTasks:
|
||||
DiscourseManager.update_groups(user)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
||||
logger.warning("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
||||
raise self.retry(countdown=60 * 10)
|
||||
logger.debug("Updated user %s discourse groups." % user)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = 'example'
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ from django.contrib import admin
|
||||
from .models import Ips4User
|
||||
|
||||
|
||||
@admin.register(Ips4User)
|
||||
class Ips4UserAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'username', 'id')
|
||||
search_fields = ('user__username', 'username', 'id')
|
||||
|
||||
admin.site.register(Ips4User, Ips4UserAdmin)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<tr>
|
||||
<td class="text-center">{{ service_name }}</td>
|
||||
<td class="text-center">{{ username }}</td>
|
||||
<td class="text-center"><a href="mumble://{{ service_url }}">{{ service_url }}</a></td>
|
||||
<td class="text-center">
|
||||
{% if username == "" %}
|
||||
<td class="text-center">{{ service_url }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'mumble:activate' %}" title="Activate" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="text-center"><a href="mumble://{{ connect_url }}">{{ service_url }}</a></td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'mumble:set_password' %}" title="Set Password" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
</a>
|
||||
@@ -17,9 +20,9 @@
|
||||
<a href="{% url 'mumble:deactivate' %}" title="Deactivate" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</a>
|
||||
<a href="mumble://{{ connect_url }}" class="btn btn-success" title="Connect">
|
||||
<a href="mumble://{{ connect_url }}" class="btn btn-success" title="Connect">
|
||||
<span class="glyphicon glyphicon-arrow-right"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
0
allianceauth/services/modules/openfire/manager.py
Executable file → Normal file
0
allianceauth/services/modules/openfire/manager.py
Executable file → Normal file
0
allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html
Executable file → Normal file
0
allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html
Executable file → Normal file
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
2
allianceauth/services/modules/phpbb3/manager.py
Executable file → Normal file
2
allianceauth/services/modules/phpbb3/manager.py
Executable file → Normal file
@@ -176,7 +176,7 @@ class Phpbb3Manager:
|
||||
logger.debug(f"Proceeding to add phpbb user {username_clean} and pwhash starting with {pwhash[0:5]}")
|
||||
# check if the username was simply revoked
|
||||
if Phpbb3Manager.check_user(username_clean):
|
||||
logger.warn("Unable to add phpbb user with username %s - already exists. Updating user instead." % username)
|
||||
logger.warning("Unable to add phpbb user with username %s - already exists. Updating user instead." % username)
|
||||
Phpbb3Manager.__update_user_info(username_clean, email, pwhash)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@ class SmfService(ServicesHook):
|
||||
if SmfTasks.has_account(user):
|
||||
SmfTasks.update_groups.delay(user.pk)
|
||||
|
||||
def sync_nickname(self, user):
|
||||
logger.debug(f"Updating {self.name} displayed name for {user}")
|
||||
|
||||
if SmfTasks.has_account(user):
|
||||
SmfTasks.update_display_name.apply_async(args=[user.pk], countdown=5) # cooldown on this task to ensure DB clean when syncing
|
||||
|
||||
def update_all_groups(self):
|
||||
logger.debug('Update all %s groups called' % self.name)
|
||||
SmfTasks.update_all_groups.delay()
|
||||
|
||||
@@ -5,11 +5,14 @@ from datetime import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
from packaging import version
|
||||
|
||||
from django.db import connections
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,6 +40,10 @@ class SmfManager:
|
||||
|
||||
SQL_DEL_USER = r"DELETE FROM %smembers where member_name = %%s" % TABLE_PREFIX
|
||||
|
||||
SQL_UPD_USER = r"UPDATE %smembers SET email_address = %%s, passwd = %%s, real_name = %%s WHERE member_name = %%s" % TABLE_PREFIX
|
||||
|
||||
SQL_UPD_DISPLAY_NAME = r"UPDATE %smembers SET real_name = %%s WHERE member_name = %%s" % TABLE_PREFIX
|
||||
|
||||
SQL_DIS_USER = r"UPDATE %smembers SET email_address = %%s, passwd = %%s WHERE member_name = %%s" % TABLE_PREFIX
|
||||
|
||||
SQL_USER_ID_FROM_USERNAME = r"SELECT id_member from %smembers WHERE member_name = %%s" % TABLE_PREFIX
|
||||
@@ -174,50 +181,75 @@ class SmfManager:
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def add_user(cls, username, email_address, groups, characterid):
|
||||
def add_user(cls, username, email_address, groups, main_character: EveCharacter) -> Tuple:
|
||||
"""
|
||||
Add a user to SMF
|
||||
:param username:
|
||||
:param email_address:
|
||||
:param groups:
|
||||
:param main_character:
|
||||
:return:
|
||||
"""
|
||||
|
||||
main_character_id = main_character.character_id
|
||||
main_character_name = main_character.character_name
|
||||
|
||||
logger.debug(
|
||||
f"Adding smf user with member_name {username}, "
|
||||
f"email_address {email_address}, "
|
||||
f"characterid {characterid}"
|
||||
f"Adding smf user with member_name: {username}, "
|
||||
f"email_address: {email_address}, "
|
||||
f"characterid: {main_character_id}, "
|
||||
f"main character: {main_character_name}"
|
||||
)
|
||||
|
||||
cursor = connections['smf'].cursor()
|
||||
username_clean = cls.santatize_username(username)
|
||||
passwd = cls.generate_random_pass()
|
||||
pwhash = cls.gen_hash(username_clean, passwd)
|
||||
logger.debug(f"Proceeding to add smf user {username} and pwhash starting with {pwhash[0:5]}")
|
||||
register_date = cls.get_current_utc_date()
|
||||
|
||||
logger.debug(f"Proceeding to add smf user {username} and pwhash starting with {pwhash[0:5]}")
|
||||
|
||||
# check if the username was simply revoked
|
||||
if cls.check_user(username) is True:
|
||||
logger.warning(f"Unable to add smf user with username {username} - already exists. Updating user instead.")
|
||||
cls.__update_user_info(username_clean, email_address, pwhash)
|
||||
logger.warning(
|
||||
f"Unable to add smf user with username {username} - "
|
||||
f"already exists. Updating user instead."
|
||||
)
|
||||
|
||||
cls.__update_user_info(
|
||||
username_clean, email_address, pwhash, main_character_name
|
||||
)
|
||||
else:
|
||||
try:
|
||||
smf_version = cls._get_current_smf_version()
|
||||
sql_add_user_arguments = [
|
||||
username_clean,
|
||||
pwhash,
|
||||
email_address,
|
||||
register_date,
|
||||
main_character_name,
|
||||
]
|
||||
|
||||
if version.parse(smf_version) < version.parse("2.1"):
|
||||
logger.debug("SMF compatibility: < 2.1")
|
||||
|
||||
cursor.execute(
|
||||
cls.SQL_ADD_USER_SMF_20,
|
||||
[username_clean, pwhash, email_address, register_date, username_clean]
|
||||
)
|
||||
cursor.execute(cls.SQL_ADD_USER_SMF_20, sql_add_user_arguments)
|
||||
else:
|
||||
logger.debug("SMF compatibility: >= 2.1")
|
||||
|
||||
cursor.execute(
|
||||
cls.SQL_ADD_USER_SMF_21,
|
||||
[username_clean, pwhash, email_address, register_date, username_clean]
|
||||
)
|
||||
cls.add_avatar(username_clean, characterid)
|
||||
cursor.execute(cls.SQL_ADD_USER_SMF_21, sql_add_user_arguments)
|
||||
|
||||
cls.add_avatar(username_clean, main_character_id)
|
||||
logger.info(f"Added smf member_name {username_clean}")
|
||||
cls.update_groups(username_clean, groups)
|
||||
except Exception as e:
|
||||
logger.warning(f"Unable to add smf user {username_clean}: {e}")
|
||||
pass
|
||||
|
||||
return username_clean, passwd
|
||||
|
||||
@classmethod
|
||||
def __update_user_info(cls, username, email_address, passwd):
|
||||
def __update_user_info(cls, username, email_address, passwd, main_character_name):
|
||||
logger.debug(
|
||||
f"Updating smf user {username} info: "
|
||||
f"username {email_address} "
|
||||
@@ -225,7 +257,9 @@ class SmfManager:
|
||||
)
|
||||
cursor = connections['smf'].cursor()
|
||||
try:
|
||||
cursor.execute(cls.SQL_DIS_USER, [email_address, passwd, username])
|
||||
cursor.execute(
|
||||
cls.SQL_UPD_USER, [email_address, passwd, main_character_name, username]
|
||||
)
|
||||
logger.info(f"Updated smf user {username} info")
|
||||
except Exception as e:
|
||||
logger.exception(f"Unable to update smf user {username} info. ({e})")
|
||||
@@ -243,6 +277,27 @@ class SmfManager:
|
||||
logger.error(f"Unable to delete smf user {username} - user not found on smf.")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def update_display_name(cls, user: User):
|
||||
logger.debug(f"Updating SMF displayed name for user {user}")
|
||||
cursor = connections['smf'].cursor()
|
||||
smf_username = user.smf.username
|
||||
|
||||
try:
|
||||
display_name = user.profile.main_character.character_name
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
f"Unable to find a main character name for {user}, skipping... ({exc})"
|
||||
)
|
||||
display_name = smf_username
|
||||
|
||||
if cls.check_user(smf_username):
|
||||
cursor.execute(cls.SQL_UPD_DISPLAY_NAME, [display_name, smf_username])
|
||||
logger.info(f"Updated displayed name for smf user {smf_username}")
|
||||
return True
|
||||
logger.error(f"Unable to update smf user {smf_username} - user not found on smf.")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def update_groups(cls, username, groups):
|
||||
userid = cls.get_user_id(username)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user