mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-05 06:36:19 +01:00
Compare commits
260 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 | ||
|
|
be2fbe862b | ||
|
|
f95bee0921 | ||
|
|
2f9ae8b054 | ||
|
|
74651dd30a | ||
|
|
9cdcd8365c | ||
|
|
f5d70a2c48 | ||
|
|
f40ebbfba4 | ||
|
|
2551a9dd64 | ||
|
|
e3b01ccbc9 | ||
|
|
267a392945 | ||
|
|
634d021bf2 | ||
|
|
4e8bfba738 | ||
|
|
297f98f046 | ||
|
|
27dad05927 | ||
|
|
697e9dd772 | ||
|
|
312951ea3f | ||
|
|
e4bf96cfb6 | ||
|
|
3bd6baa8f9 | ||
|
|
06e38dcd93 | ||
|
|
f47b9eee5b | ||
|
|
0d4cab66b2 | ||
|
|
dc23ee8ad2 | ||
|
|
65f2efc890 | ||
|
|
def30900b4 | ||
|
|
d7fabccddd | ||
|
|
45289e1d17 | ||
|
|
e7bafaa4d8 | ||
|
|
ba3f1507be | ||
|
|
7b9bf08aa3 | ||
|
|
360458f574 | ||
|
|
def6431052 | ||
|
|
a47bd8d7c7 | ||
|
|
22a270aedb | ||
|
|
54bce4315b | ||
|
|
c930f7bbeb | ||
|
|
8c1f06d7b8 | ||
|
|
815b6fa030 | ||
|
|
7c05217900 | ||
|
|
64ee273953 | ||
|
|
d8c2944966 | ||
|
|
7669c9e55d | ||
|
|
71c9faaf28 | ||
|
|
236c70316c | ||
|
|
0d0686f58a | ||
|
|
3706a1aedf | ||
|
|
47f1b77320 | ||
|
|
8dec242a93 | ||
|
|
6b934060dd | ||
|
|
ff88a16163 | ||
|
|
e81a66b74b | ||
|
|
2ff200c566 | ||
|
|
091a2637ea | ||
|
|
6c7729308c | ||
|
|
0195ef23d5 | ||
|
|
a7afa4a0c3 | ||
|
|
004100091f | ||
|
|
20231ce198 | ||
|
|
0851a6d085 | ||
|
|
0cd36ad5bc | ||
|
|
7618dd0f91 | ||
|
|
cf49a2cb65 | ||
|
|
cbdce18633 | ||
|
|
a0d14eb1d3 | ||
|
|
53ce4d2453 | ||
|
|
1ddb041d6d | ||
|
|
43cbfd1c47 | ||
|
|
b9a8495a43 | ||
|
|
e296477880 | ||
|
|
bd5c2d8cbc | ||
|
|
ccd40d5c68 | ||
|
|
113977b19f |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -69,11 +69,7 @@ celerybeat-schedule
|
|||||||
#gitlab configs
|
#gitlab configs
|
||||||
.gitlab/
|
.gitlab/
|
||||||
|
|
||||||
#transifex
|
|
||||||
.tx/
|
|
||||||
|
|
||||||
#other
|
#other
|
||||||
.flake8
|
.flake8
|
||||||
.pylintrc
|
.pylintrc
|
||||||
Makefile
|
Makefile
|
||||||
.isort.cfg
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ before_script:
|
|||||||
pre-commit-check:
|
pre-commit-check:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
stage: pre-commit
|
stage: pre-commit
|
||||||
image: python:3.8-bullseye
|
image: python:3.10-bullseye
|
||||||
variables:
|
variables:
|
||||||
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||||
cache:
|
cache:
|
||||||
@@ -89,7 +89,7 @@ test-3.10-core:
|
|||||||
|
|
||||||
test-3.11-core:
|
test-3.11-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.11-rc-bullseye
|
image: python:3.11-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py311-core
|
- tox -e py311-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -98,6 +98,18 @@ test-3.11-core:
|
|||||||
coverage_report:
|
coverage_report:
|
||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage.xml
|
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
|
allow_failure: true
|
||||||
|
|
||||||
test-3.8-all:
|
test-3.8-all:
|
||||||
@@ -138,7 +150,7 @@ test-3.10-all:
|
|||||||
|
|
||||||
test-3.11-all:
|
test-3.11-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.11-rc-bullseye
|
image: python:3.11-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py311-all
|
- tox -e py311-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -147,6 +159,19 @@ test-3.11-all:
|
|||||||
coverage_report:
|
coverage_report:
|
||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage.xml
|
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
|
allow_failure: true
|
||||||
|
|
||||||
build-test:
|
build-test:
|
||||||
@@ -213,6 +238,8 @@ build-image:
|
|||||||
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
|
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_TAG
|
- if: $CI_COMMIT_TAG
|
||||||
|
when: delayed
|
||||||
|
start_in: 10 minutes
|
||||||
|
|
||||||
build-image-dev:
|
build-image-dev:
|
||||||
before_script: []
|
before_script: []
|
||||||
|
|||||||
@@ -5,35 +5,75 @@
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.3.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-case-conflict
|
# Identify invalid files
|
||||||
- id: check-json
|
- id: check-ast
|
||||||
- id: check-xml
|
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: fix-byte-order-marker
|
- id: check-json
|
||||||
- id: trailing-whitespace
|
- id: check-toml
|
||||||
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
|
- id: check-xml
|
||||||
- id: end-of-file-fixer
|
|
||||||
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
|
# git checks
|
||||||
- id: mixed-line-ending
|
- id: check-merge-conflict
|
||||||
args: [ '--fix=lf' ]
|
- 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
|
- 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
|
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||||
rev: 2.4.0
|
rev: 2.7.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: editorconfig-checker
|
- id: editorconfig-checker
|
||||||
exclude: ^(LICENSE|allianceauth\/static\/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
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.34.0
|
rev: v3.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [ --py38-plus ]
|
args: [ --py38-plus ]
|
||||||
|
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
|
||||||
rev: v1.20.1
|
|
||||||
hooks:
|
|
||||||
- id: setup-cfg-fmt
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ build:
|
|||||||
apt_packages:
|
apt_packages:
|
||||||
- redis
|
- redis
|
||||||
tools:
|
tools:
|
||||||
python: "3.10"
|
python: "3.8"
|
||||||
|
|
||||||
# Build documentation in the docs/ directory with Sphinx
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
sphinx:
|
sphinx:
|
||||||
|
|||||||
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
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
include LICENSE
|
|
||||||
include README.md
|
|
||||||
include MANIFEST.in
|
|
||||||
graft allianceauth
|
|
||||||
|
|
||||||
global-exclude __pycache__
|
|
||||||
global-exclude *.py[co]
|
|
||||||
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)
|
- 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).
|
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/)
|
- [Aaron Kable](https://gitlab.com/aaronkable/)
|
||||||
- [Ariel Rin](https://gitlab.com/soratidus999/)
|
- [Ariel Rin](https://gitlab.com/soratidus999/)
|
||||||
- [Basraah](https://gitlab.com/basraah/)
|
|
||||||
- [Col Crunch](https://gitlab.com/colcrunch/)
|
- [Col Crunch](https://gitlab.com/colcrunch/)
|
||||||
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
|
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
|
||||||
|
- [Rounon Dax](https://gitlab.com/ppfeufer)
|
||||||
|
- [snipereagle1](https://gitlab.com/mckernanin)
|
||||||
|
|
||||||
### Former Developers
|
### Former Developers
|
||||||
|
|
||||||
- [Adarnof](https://gitlab.com/adarnof/)
|
- [Adarnof](https://gitlab.com/adarnof/)
|
||||||
|
- [Basraah](https://gitlab.com/basraah/)
|
||||||
|
|
||||||
### Beta Testers / Bug Fixers
|
### 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
|
# This will make sure the app is always imported when
|
||||||
# Django starts so that shared_task will use this app.
|
# Django starts so that shared_task will use this app.
|
||||||
|
|
||||||
__version__ = '3.0.0b1'
|
__version__ = '3.6.1'
|
||||||
__title__ = 'Alliance Auth'
|
__title__ = 'Alliance Auth'
|
||||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||||
NAME = f'{__title__} v{__version__}'
|
NAME = f'{__title__} v{__version__}'
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ from uuid import uuid4
|
|||||||
class AnalyticsIdentifier(models.Model):
|
class AnalyticsIdentifier(models.Model):
|
||||||
|
|
||||||
identifier = models.UUIDField(default=uuid4,
|
identifier = models.UUIDField(default=uuid4,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.pk and AnalyticsIdentifier.objects.exists():
|
if not self.pk and AnalyticsIdentifier.objects.exists():
|
||||||
# Force a single object
|
# Force a single object
|
||||||
raise ValidationError('There is can be only one \
|
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
|
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ from urllib.parse import parse_qs
|
|||||||
|
|
||||||
import requests_mock
|
import requests_mock
|
||||||
|
|
||||||
from django.test import TestCase, override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from allianceauth.analytics.tasks import ANALYTICS_URL
|
from allianceauth.analytics.tasks import ANALYTICS_URL
|
||||||
from allianceauth.eveonline.tasks import update_character
|
from allianceauth.eveonline.tasks import update_character
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
from allianceauth.utils.testing import NoSocketsTestCase
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
class TestAnalyticsForViews(TestCase):
|
class TestAnalyticsForViews(NoSocketsTestCase):
|
||||||
@override_settings(ANALYTICS_DISABLED=False)
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
def test_should_run_analytics(self, requests_mocker):
|
def test_should_run_analytics(self, requests_mocker):
|
||||||
# given
|
# given
|
||||||
@@ -40,7 +41,7 @@ class TestAnalyticsForViews(TestCase):
|
|||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
class TestAnalyticsForTasks(TestCase):
|
class TestAnalyticsForTasks(NoSocketsTestCase):
|
||||||
@override_settings(ANALYTICS_DISABLED=False)
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||||
def test_should_run_analytics_for_successful_task(
|
def test_should_run_analytics_for_successful_task(
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
|
import requests_mock
|
||||||
|
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
from allianceauth.analytics.tasks import (
|
from allianceauth.analytics.tasks import (
|
||||||
analytics_event,
|
analytics_event,
|
||||||
send_ga_tracking_celery_event,
|
send_ga_tracking_celery_event,
|
||||||
send_ga_tracking_web_view)
|
send_ga_tracking_web_view)
|
||||||
from django.test.testcases import TestCase
|
from allianceauth.utils.testing import NoSocketsTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestAnalyticsTasks(TestCase):
|
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/collect'
|
||||||
def test_analytics_event(self):
|
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
class TestAnalyticsTasks(NoSocketsTestCase):
|
||||||
|
def test_analytics_event(self, requests_mocker):
|
||||||
|
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||||
analytics_event(
|
analytics_event(
|
||||||
category='allianceauth.analytics',
|
category='allianceauth.analytics',
|
||||||
action='send_tests',
|
action='send_tests',
|
||||||
@@ -14,15 +24,19 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
value=1,
|
value=1,
|
||||||
event_type='Stats')
|
event_type='Stats')
|
||||||
|
|
||||||
def test_send_ga_tracking_web_view_sent(self):
|
def test_send_ga_tracking_web_view_sent(self, requests_mocker):
|
||||||
# This test sends if the event SENDS to google
|
"""This test sends if the event SENDS to google.
|
||||||
# Not if it was successful
|
Not if it was successful.
|
||||||
|
"""
|
||||||
|
# given
|
||||||
|
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
page = '/index/'
|
page = '/index/'
|
||||||
title = 'Hello World'
|
title = 'Hello World'
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
# when
|
||||||
response = send_ga_tracking_web_view(
|
response = send_ga_tracking_web_view(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -30,15 +44,23 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
title,
|
title,
|
||||||
locale,
|
locale,
|
||||||
useragent)
|
useragent)
|
||||||
|
# then
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_send_ga_tracking_web_view_success(self):
|
def test_send_ga_tracking_web_view_success(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||||
|
json={"hitParsingResult":[{'valid': True}]}
|
||||||
|
)
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
page = '/index/'
|
page = '/index/'
|
||||||
title = 'Hello World'
|
title = 'Hello World'
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
# when
|
||||||
json_response = send_ga_tracking_web_view(
|
json_response = send_ga_tracking_web_view(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -46,15 +68,42 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
title,
|
title,
|
||||||
locale,
|
locale,
|
||||||
useragent).json()
|
useragent).json()
|
||||||
|
# then
|
||||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
|
||||||
def test_send_ga_tracking_web_view_invalid_token(self):
|
def test_send_ga_tracking_web_view_invalid_token(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||||
|
json={
|
||||||
|
"hitParsingResult":[
|
||||||
|
{
|
||||||
|
'valid': False,
|
||||||
|
'parserMessage': [
|
||||||
|
{
|
||||||
|
'messageType': 'INFO',
|
||||||
|
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
|
||||||
|
'messageCode': 'VALUE_MODIFIED'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'messageType': 'ERROR',
|
||||||
|
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
|
||||||
|
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
page = '/index/'
|
page = '/index/'
|
||||||
title = 'Hello World'
|
title = 'Hello World'
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
# when
|
||||||
json_response = send_ga_tracking_web_view(
|
json_response = send_ga_tracking_web_view(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -62,18 +111,25 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
title,
|
title,
|
||||||
locale,
|
locale,
|
||||||
useragent).json()
|
useragent).json()
|
||||||
|
# then
|
||||||
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||||
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
self.assertEqual(
|
||||||
|
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
|
||||||
|
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
|
||||||
|
)
|
||||||
|
|
||||||
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||||
|
|
||||||
def test_send_ga_tracking_celery_event_sent(self):
|
def test_send_ga_tracking_celery_event_sent(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
category = 'test'
|
category = 'test'
|
||||||
action = 'test'
|
action = 'test'
|
||||||
label = 'test'
|
label = 'test'
|
||||||
value = '1'
|
value = '1'
|
||||||
|
# when
|
||||||
response = send_ga_tracking_celery_event(
|
response = send_ga_tracking_celery_event(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -81,15 +137,23 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
value)
|
value)
|
||||||
|
# then
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_send_ga_tracking_celery_event_success(self):
|
def test_send_ga_tracking_celery_event_success(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||||
|
json={"hitParsingResult":[{'valid': True}]}
|
||||||
|
)
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
category = 'test'
|
category = 'test'
|
||||||
action = 'test'
|
action = 'test'
|
||||||
label = 'test'
|
label = 'test'
|
||||||
value = '1'
|
value = '1'
|
||||||
|
# when
|
||||||
json_response = send_ga_tracking_celery_event(
|
json_response = send_ga_tracking_celery_event(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -97,15 +161,42 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
value).json()
|
value).json()
|
||||||
|
# then
|
||||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
|
||||||
def test_send_ga_tracking_celery_event_invalid_token(self):
|
def test_send_ga_tracking_celery_event_invalid_token(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||||
|
json={
|
||||||
|
"hitParsingResult":[
|
||||||
|
{
|
||||||
|
'valid': False,
|
||||||
|
'parserMessage': [
|
||||||
|
{
|
||||||
|
'messageType': 'INFO',
|
||||||
|
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
|
||||||
|
'messageCode': 'VALUE_MODIFIED'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'messageType': 'ERROR',
|
||||||
|
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
|
||||||
|
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
category = 'test'
|
category = 'test'
|
||||||
action = 'test'
|
action = 'test'
|
||||||
label = 'test'
|
label = 'test'
|
||||||
value = '1'
|
value = '1'
|
||||||
|
# when
|
||||||
json_response = send_ga_tracking_celery_event(
|
json_response = send_ga_tracking_celery_event(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -113,7 +204,9 @@ class TestAnalyticsTasks(TestCase):
|
|||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
value).json()
|
value).json()
|
||||||
|
# then
|
||||||
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||||
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
self.assertEqual(
|
||||||
|
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
|
||||||
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
|
||||||
|
)
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
"all": ("authentication/css/admin.css",)
|
"all": ("allianceauth/authentication/css/admin.css",)
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
@@ -542,7 +542,7 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
"all": ("authentication/css/admin.css",)
|
"all": ("allianceauth/authentication/css/admin.css",)
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from .models import UserProfile, CharacterOwnership, OwnershipRecord
|
from .models import UserProfile, CharacterOwnership, OwnershipRecord
|
||||||
|
|
||||||
@@ -37,7 +38,13 @@ class StateBackend(ModelBackend):
|
|||||||
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
|
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
|
||||||
if ownership.owner_hash == token.character_owner_hash:
|
if ownership.owner_hash == token.character_owner_hash:
|
||||||
logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}')
|
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:
|
else:
|
||||||
logger.debug(f'{token.character_name} has changed ownership. Creating new user account.')
|
logger.debug(f'{token.character_name} has changed ownership. Creating new user account.')
|
||||||
ownership.delete()
|
ownership.delete()
|
||||||
@@ -57,13 +64,20 @@ class StateBackend(ModelBackend):
|
|||||||
if records.exists():
|
if records.exists():
|
||||||
# we've seen this character owner before. Re-attach to their old user account
|
# we've seen this character owner before. Re-attach to their old user account
|
||||||
user = records[0].user
|
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
|
token.user = user
|
||||||
co = CharacterOwnership.objects.create_by_token(token)
|
co = CharacterOwnership.objects.create_by_token(token)
|
||||||
logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}')
|
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
|
# set this as their main by default as they have none
|
||||||
user.profile.main_character = co.character
|
user.profile.main_character = co.character
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
return user
|
return user
|
||||||
logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.')
|
logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.')
|
||||||
return self.create_user(token)
|
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 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 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.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
|
|
||||||
|
|
||||||
def user_has_main_character(user):
|
def user_has_main_character(user):
|
||||||
return bool(user.profile.main_character)
|
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)
|
url_list, app_name, namespace = include(urls)
|
||||||
|
|
||||||
def process_patterns(url_patterns):
|
def process_patterns(url_patterns):
|
||||||
@@ -22,6 +32,8 @@ def decorate_url_patterns(urls, decorator):
|
|||||||
process_patterns(pattern.url_patterns)
|
process_patterns(pattern.url_patterns)
|
||||||
else:
|
else:
|
||||||
# this is a pattern
|
# this is a pattern
|
||||||
|
if excluded_views and pattern.lookup_str in excluded_views:
|
||||||
|
return
|
||||||
pattern.callback = decorator(pattern.callback)
|
pattern.callback = decorator(pattern.callback)
|
||||||
|
|
||||||
process_patterns(url_list)
|
process_patterns(url_list)
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
from django.conf.urls import include
|
|
||||||
|
|
||||||
from allianceauth.authentication import views
|
from allianceauth.authentication import views
|
||||||
from django.urls import re_path
|
from django.urls import include, re_path, path
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),
|
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.")
|
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,
|
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,
|
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,
|
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,
|
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.")
|
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
||||||
|
|
||||||
objects = StateManager()
|
objects = StateManager()
|
||||||
@@ -63,6 +63,22 @@ class UserProfile(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ('change',)
|
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 = models.OneToOneField(
|
||||||
User,
|
User,
|
||||||
related_name='profile',
|
related_name='profile',
|
||||||
@@ -76,20 +92,9 @@ class UserProfile(models.Model):
|
|||||||
State,
|
State,
|
||||||
on_delete=models.SET_DEFAULT,
|
on_delete=models.SET_DEFAULT,
|
||||||
default=get_guest_state_pk)
|
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 = models.CharField(
|
||||||
_("Language"), max_length=10,
|
_("Language"), max_length=10,
|
||||||
choices=LANGUAGE_CHOICES,
|
choices=Language.choices,
|
||||||
blank=True,
|
blank=True,
|
||||||
default='')
|
default='')
|
||||||
night_mode = models.BooleanField(
|
night_mode = models.BooleanField(
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
CSS for allianceauth admin site
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* styling for profile pic */
|
||||||
|
.img-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-user_profile_pic {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tooltip */
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover::after {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
CSS for allianceauth admin site
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* styling for profile pic */
|
|
||||||
.img-circle {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.column-user_profile_pic {
|
|
||||||
width: 1px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip */
|
|
||||||
.tooltip {
|
|
||||||
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 ;
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,44 @@
|
|||||||
from collections import namedtuple
|
"""Counters for Task Statistics."""
|
||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
from .event_series import EventSeries
|
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")
|
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
|
||||||
retried_tasks = EventSeries("RETRIED_TASKS")
|
retried_tasks = EventSeries("RETRIED_TASKS")
|
||||||
failed_tasks = EventSeries("FAILED_TASKS")
|
failed_tasks = EventSeries("FAILED_TASKS")
|
||||||
|
running_tasks = ItemCounter("running_tasks")
|
||||||
|
|
||||||
|
|
||||||
_TaskCounts = namedtuple(
|
class _TaskCounts(NamedTuple):
|
||||||
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
|
succeeded: int
|
||||||
)
|
retried: int
|
||||||
|
failed: int
|
||||||
|
total: int
|
||||||
|
earliest_task: Optional[dt.datetime]
|
||||||
|
hours: int
|
||||||
|
running: int
|
||||||
|
|
||||||
|
|
||||||
def dashboard_results(hours: int) -> _TaskCounts:
|
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:
|
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
|
||||||
my_earliest = events.first_event(earliest=earliest)
|
my_earliest = events.first_event(earliest=earliest)
|
||||||
return [my_earliest] if my_earliest else []
|
return [my_earliest] if my_earliest else []
|
||||||
|
|
||||||
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||||
earliest_events = list()
|
earliest_events = []
|
||||||
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
||||||
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
||||||
retried_count = retried_tasks.count(earliest=earliest)
|
retried_count = retried_tasks.count(earliest=earliest)
|
||||||
earliest_events += earliest_if_exists(retried_tasks, earliest)
|
earliest_events += earliest_if_exists(retried_tasks, earliest)
|
||||||
failed_count = failed_tasks.count(earliest=earliest)
|
failed_count = failed_tasks.count(earliest=earliest)
|
||||||
earliest_events += earliest_if_exists(failed_tasks, earliest)
|
earliest_events += earliest_if_exists(failed_tasks, earliest)
|
||||||
|
running_count = running_tasks.value()
|
||||||
return _TaskCounts(
|
return _TaskCounts(
|
||||||
succeeded=succeeded_count,
|
succeeded=succeeded_count,
|
||||||
retried=retried_count,
|
retried=retried_count,
|
||||||
@@ -37,4 +46,5 @@ def dashboard_results(hours: int) -> _TaskCounts:
|
|||||||
total=succeeded_count + retried_count + failed_count,
|
total=succeeded_count + retried_count + failed_count,
|
||||||
earliest_task=min(earliest_events) if earliest_events else None,
|
earliest_task=min(earliest_events) if earliest_events else None,
|
||||||
hours=hours,
|
hours=hours,
|
||||||
|
running=running_count,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,61 +1,31 @@
|
|||||||
|
"""Event series for Task Statistics."""
|
||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
from redis import Redis, RedisError
|
from redis import Redis
|
||||||
|
|
||||||
from django_redis import get_redis_connection
|
from .helpers import get_redis_client_or_stub
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class EventSeries:
|
||||||
"""API for recording and analyzing a series of events."""
|
"""API for recording and analyzing a series of events."""
|
||||||
|
|
||||||
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
||||||
|
|
||||||
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
def __init__(self, key_id: str, redis: Optional[Redis] = None) -> None:
|
||||||
self._redis = get_redis_connection("default") if not redis else redis
|
self._redis = get_redis_client_or_stub() 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()
|
|
||||||
self._key_id = str(key_id)
|
self._key_id = str(key_id)
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_disabled(self):
|
def is_disabled(self):
|
||||||
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
"""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
|
@property
|
||||||
def _key_counter(self):
|
def _key_counter(self):
|
||||||
@@ -73,8 +43,8 @@ class EventSeries:
|
|||||||
"""
|
"""
|
||||||
if not event_time:
|
if not event_time:
|
||||||
event_time = dt.datetime.utcnow()
|
event_time = dt.datetime.utcnow()
|
||||||
id = self._redis.incr(self._key_counter)
|
my_id = self._redis.incr(self._key_counter)
|
||||||
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
|
self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})
|
||||||
|
|
||||||
def all(self) -> List[dt.datetime]:
|
def all(self) -> List[dt.datetime]:
|
||||||
"""List of all known events."""
|
"""List of all known events."""
|
||||||
@@ -95,15 +65,15 @@ class EventSeries:
|
|||||||
self._redis.delete(self._key_counter)
|
self._redis.delete(self._key_counter)
|
||||||
|
|
||||||
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
|
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:
|
Args:
|
||||||
- earliest: Date of first events to count(inclusive), or -infinite if not specified
|
- 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
|
- latest: Date of last events to count(inclusive), or +infinite if not specified
|
||||||
"""
|
"""
|
||||||
min = "-inf" if not earliest else earliest.timestamp()
|
minimum = "-inf" if not earliest else earliest.timestamp()
|
||||||
max = "+inf" if not latest else latest.timestamp()
|
maximum = "+inf" if not latest else latest.timestamp()
|
||||||
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
|
return self._redis.zcount(self._key_sorted_set, min=minimum, max=maximum)
|
||||||
|
|
||||||
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
|
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
|
||||||
"""Date/Time of first event. Returns `None` if series has no events.
|
"""Date/Time of first event. Returns `None` if series has no events.
|
||||||
@@ -111,10 +81,10 @@ class EventSeries:
|
|||||||
Args:
|
Args:
|
||||||
- earliest: Date of first events to count(inclusive), or any if not specified
|
- 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(
|
event = self._redis.zrangebyscore(
|
||||||
self._key_sorted_set,
|
self._key_sorted_set,
|
||||||
min,
|
minimum,
|
||||||
"+inf",
|
"+inf",
|
||||||
withscores=True,
|
withscores=True,
|
||||||
start=0,
|
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 (
|
from celery.signals import (
|
||||||
task_failure,
|
task_failure, task_internal_error, task_postrun, task_prerun, task_retry,
|
||||||
task_internal_error,
|
task_success, worker_ready,
|
||||||
task_retry,
|
|
||||||
task_success,
|
|
||||||
worker_ready
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.conf import settings
|
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():
|
def reset_counters():
|
||||||
@@ -16,9 +17,11 @@ def reset_counters():
|
|||||||
succeeded_tasks.clear()
|
succeeded_tasks.clear()
|
||||||
failed_tasks.clear()
|
failed_tasks.clear()
|
||||||
retried_tasks.clear()
|
retried_tasks.clear()
|
||||||
|
running_tasks.reset()
|
||||||
|
|
||||||
|
|
||||||
def is_enabled() -> bool:
|
def is_enabled() -> bool:
|
||||||
|
"""Return True if task statistics are enabled, else return False."""
|
||||||
return not bool(
|
return not bool(
|
||||||
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
|
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):
|
def record_task_internal_error(*args, **kwargs):
|
||||||
if is_enabled():
|
if is_enabled():
|
||||||
failed_tasks.add()
|
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,
|
succeeded_tasks,
|
||||||
retried_tasks,
|
retried_tasks,
|
||||||
failed_tasks,
|
failed_tasks,
|
||||||
|
running_tasks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDashboardResults(TestCase):
|
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
|
# given
|
||||||
earliest_task = now() - dt.timedelta(minutes=15)
|
earliest_task = now() - dt.timedelta(minutes=15)
|
||||||
|
|
||||||
succeeded_tasks.clear()
|
succeeded_tasks.clear()
|
||||||
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
succeeded_tasks.add(earliest_task)
|
succeeded_tasks.add(earliest_task)
|
||||||
succeeded_tasks.add()
|
succeeded_tasks.add()
|
||||||
succeeded_tasks.add()
|
succeeded_tasks.add()
|
||||||
|
|
||||||
retried_tasks.clear()
|
retried_tasks.clear()
|
||||||
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
retried_tasks.add(now() - dt.timedelta(seconds=30))
|
retried_tasks.add(now() - dt.timedelta(seconds=30))
|
||||||
retried_tasks.add()
|
retried_tasks.add()
|
||||||
|
|
||||||
failed_tasks.clear()
|
failed_tasks.clear()
|
||||||
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
failed_tasks.add()
|
failed_tasks.add()
|
||||||
|
|
||||||
|
running_tasks.reset(8)
|
||||||
# when
|
# when
|
||||||
results = dashboard_results(hours=1)
|
results = dashboard_results(hours=1)
|
||||||
# then
|
# then
|
||||||
@@ -35,12 +41,14 @@ class TestDashboardResults(TestCase):
|
|||||||
self.assertEqual(results.failed, 1)
|
self.assertEqual(results.failed, 1)
|
||||||
self.assertEqual(results.total, 6)
|
self.assertEqual(results.total, 6)
|
||||||
self.assertEqual(results.earliest_task, earliest_task)
|
self.assertEqual(results.earliest_task, earliest_task)
|
||||||
|
self.assertEqual(results.running, 8)
|
||||||
|
|
||||||
def test_should_work_with_no_data(self):
|
def test_should_work_with_no_data(self):
|
||||||
# given
|
# given
|
||||||
succeeded_tasks.clear()
|
succeeded_tasks.clear()
|
||||||
retried_tasks.clear()
|
retried_tasks.clear()
|
||||||
failed_tasks.clear()
|
failed_tasks.clear()
|
||||||
|
running_tasks.reset()
|
||||||
# when
|
# when
|
||||||
results = dashboard_results(hours=1)
|
results = dashboard_results(hours=1)
|
||||||
# then
|
# then
|
||||||
@@ -49,3 +57,4 @@ class TestDashboardResults(TestCase):
|
|||||||
self.assertEqual(results.failed, 0)
|
self.assertEqual(results.failed, 0)
|
||||||
self.assertEqual(results.total, 0)
|
self.assertEqual(results.total, 0)
|
||||||
self.assertIsNone(results.earliest_task)
|
self.assertIsNone(results.earliest_task)
|
||||||
|
self.assertEqual(results.running, 0)
|
||||||
|
|||||||
@@ -1,48 +1,19 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
from redis import RedisError
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from allianceauth.authentication.task_statistics.event_series import (
|
from allianceauth.authentication.task_statistics.event_series import (
|
||||||
EventSeries,
|
EventSeries,
|
||||||
_RedisStub,
|
|
||||||
)
|
)
|
||||||
|
from allianceauth.authentication.task_statistics.helpers import _RedisStub
|
||||||
|
|
||||||
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
||||||
|
|
||||||
|
|
||||||
class TestEventSeries(TestCase):
|
class TestEventSeries(TestCase):
|
||||||
def test_should_abort_without_redis_client(self):
|
|
||||||
# when
|
|
||||||
with patch(MODULE_PATH + ".get_redis_connection") 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_connection") as mock_get_redis_connection:
|
|
||||||
mock_get_redis_connection.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_connection") as mock_get_redis_connection:
|
|
||||||
mock_get_redis_connection.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):
|
def test_should_add_event(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = EventSeries("dummy")
|
||||||
@@ -166,3 +137,15 @@ class TestEventSeries(TestCase):
|
|||||||
results = events.all()
|
results = events.all()
|
||||||
# then
|
# then
|
||||||
self.assertEqual(len(results), 2)
|
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(
|
@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):
|
class TestTaskSignals(TestCase):
|
||||||
fixtures = ["disable_analytics"]
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
def test_should_record_successful_task(self):
|
def setUp(self) -> None:
|
||||||
# given
|
|
||||||
succeeded_tasks.clear()
|
succeeded_tasks.clear()
|
||||||
retried_tasks.clear()
|
retried_tasks.clear()
|
||||||
failed_tasks.clear()
|
failed_tasks.clear()
|
||||||
|
|
||||||
|
def test_should_record_successful_task(self):
|
||||||
# when
|
# when
|
||||||
with patch(
|
with patch(
|
||||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
@@ -39,10 +40,6 @@ class TestTaskSignals(TestCase):
|
|||||||
self.assertEqual(failed_tasks.count(), 0)
|
self.assertEqual(failed_tasks.count(), 0)
|
||||||
|
|
||||||
def test_should_record_retried_task(self):
|
def test_should_record_retried_task(self):
|
||||||
# given
|
|
||||||
succeeded_tasks.clear()
|
|
||||||
retried_tasks.clear()
|
|
||||||
failed_tasks.clear()
|
|
||||||
# when
|
# when
|
||||||
with patch(
|
with patch(
|
||||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
@@ -55,10 +52,6 @@ class TestTaskSignals(TestCase):
|
|||||||
self.assertEqual(retried_tasks.count(), 1)
|
self.assertEqual(retried_tasks.count(), 1)
|
||||||
|
|
||||||
def test_should_record_failed_task(self):
|
def test_should_record_failed_task(self):
|
||||||
# given
|
|
||||||
succeeded_tasks.clear()
|
|
||||||
retried_tasks.clear()
|
|
||||||
failed_tasks.clear()
|
|
||||||
# when
|
# when
|
||||||
with patch(
|
with patch(
|
||||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
|
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
|
||||||
@@ -15,9 +14,9 @@
|
|||||||
<div class="panel panel-primary" style="height:100%">
|
<div class="panel panel-primary" style="height:100%">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
{% blocktrans with state=request.user.profile.state %}
|
{% blocktranslate with state=request.user.profile.state %}
|
||||||
Main Character (State: {{ state }})
|
Main Character (State: {{ state }})
|
||||||
{% endblocktrans %}
|
{% endblocktranslate %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<img class="ra-avatar"src="{{ main.portrait_url_128 }}">
|
<img class="ra-avatar" src="{{ main.portrait_url_128 }}" alt="{{ main.character_name }}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -40,7 +39,7 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<img class="ra-avatar"src="{{ main.corporation_logo_url_128 }}">
|
<img class="ra-avatar" src="{{ main.corporation_logo_url_128 }}" alt="{{ main.corporation_name }}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -53,7 +52,7 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<img class="ra-avatar"src="{{ main.alliance_logo_url_128 }}">
|
<img class="ra-avatar" src="{{ main.alliance_logo_url_128 }}" alt="{{ main.alliance_name }}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<img class="ra-avatar"src="{{ main.faction_logo_url_128 }}">
|
<img class="ra-avatar" src="{{ main.faction_logo_url_128 }}" alt="{{ main.faction_name }}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -76,13 +75,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="table visible-xs-block">
|
<div class="table visible-xs-block">
|
||||||
<p>
|
<p>
|
||||||
<img class="ra-avatar" src="{{ main.portrait_url_64 }}">
|
<img class="ra-avatar" src="{{ main.portrait_url_64 }}" alt="{{ main.corporation_name }}">
|
||||||
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}">
|
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}" alt="{{ main.corporation_name }}">
|
||||||
{% if main.alliance_id %}
|
{% if main.alliance_id %}
|
||||||
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}">
|
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}" alt="{{ main.alliance_name }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if main.faction_id %}
|
{% if main.faction_id %}
|
||||||
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}">
|
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}" alt="{{ main.faction_name }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -104,13 +103,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 button-wrapper">
|
<div class="col-sm-6">
|
||||||
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
<p>
|
||||||
title="Add Character">{% translate 'Add Character' %}</a>
|
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
||||||
|
title="Add Character">{% translate 'Add Character' %}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 button-wrapper">
|
<div class="col-sm-6">
|
||||||
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
|
<p>
|
||||||
title="Change Main Character">{% translate "Change Main" %}</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +125,7 @@
|
|||||||
<h3 class="panel-title">{% translate "Group Memberships" %}</h3>
|
<h3 class="panel-title">{% translate "Group Memberships" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
|
<div style="height: 240px;overflow-y:auto;">
|
||||||
<table class="table table-aa">
|
<table class="table table-aa">
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -155,11 +158,12 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for char in characters %}
|
{% for char in characters %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
|
<td class="text-center">
|
||||||
|
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ char.character_name }}</td>
|
<td class="text-center">{{ char.character_name }}</td>
|
||||||
<td class="text-center">{{ char.corporation_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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -169,7 +173,7 @@
|
|||||||
{% for char in characters %}
|
{% for char in characters %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center" style="vertical-align: middle">
|
<td class="text-center" style="vertical-align: middle">
|
||||||
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
|
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="vertical-align: middle; width: 100%">
|
<td class="text-center" style="vertical-align: middle; width: 100%">
|
||||||
<strong>{{ char.character_name }}</strong><br>
|
<strong>{{ char.character_name }}</strong><br>
|
||||||
|
|||||||
@@ -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 %}
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@@ -7,7 +8,7 @@
|
|||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
<meta property="og:title" content="{{ SITE_NAME }}">
|
<meta property="og:title" content="{{ SITE_NAME }}">
|
||||||
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static '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.">
|
<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' %}
|
{% include 'allianceauth/icons.html' %}
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat center center fixed;
|
background: url('{% static 'allianceauth/authentication/img/background.jpg' %}') no-repeat center center fixed;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-moz-background-size: cover;
|
-moz-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
.panel-transparent {
|
.panel-transparent {
|
||||||
background: rgba(48, 48, 48, 0.7);
|
background: rgba(48, 48, 48, 0.7);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
padding-bottom: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container" style="margin-top:150px">
|
<div class="container" style="margin-top:150px;">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% get_language_info_list for LANGUAGES as languages %}
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||||
{{ language.name_local }} ({{ language.code }})
|
{{ language.name_local|capfirst }} ({{ language.code }})
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% block page_title %}{% translate "Login" %}{% endblock %}
|
{% block page_title %}{% translate "Login" %}{% endblock %}
|
||||||
|
|
||||||
{% block middle_box_content %}
|
{% 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 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
|
<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>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{% extends 'public/base.html' %}
|
{% extends 'public/base.html' %}
|
||||||
{% load static %}
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-md-4 col-md-offset-4">
|
<div class="col-md-4 col-md-offset-4">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -7,6 +9,7 @@
|
|||||||
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
|
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="panel panel-default panel-transparent">
|
<div class="panel panel-default panel-transparent">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@@ -14,10 +17,25 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'public/lang_select.html' %}
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_include %}
|
{% block extra_include %}
|
||||||
{% include 'bundles/bootstrap-js.html' %}
|
{% include 'bundles/bootstrap-js.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends 'public/base.html' %}
|
{% extends 'public/base.html' %}
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
|
||||||
{% blocktrans trimmed %}You're receiving this email because you requested a password reset for your
|
|
||||||
user account.{% endblocktrans %}
|
|
||||||
|
|
||||||
{% translate "Please go to the following page and choose a new password:" %}
|
|
||||||
{% block reset_link %}
|
|
||||||
{{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
|
||||||
{% endblock %}
|
|
||||||
{% translate "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
|
||||||
|
|
||||||
{% translate "Thanks for using our site!" %}
|
|
||||||
|
|
||||||
{% blocktrans %}Your IT Team{% endblocktrans %}
|
|
||||||
|
|
||||||
{% endautoescape %}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{% extends 'public/middle_box.html' %}
|
|
||||||
{% load bootstrap %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
{% block page_title %}{% translate "Register" %}{% endblock %}
|
|
||||||
{% block middle_box_content %}
|
|
||||||
<form class="form-signin" role="form" action="" method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|bootstrap }}
|
|
||||||
<br>
|
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
|
|
||||||
<br>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -116,10 +116,17 @@ class TestAuthenticate(TestCase):
|
|||||||
user = StateBackend().authenticate(token=t)
|
user = StateBackend().authenticate(token=t)
|
||||||
self.assertEqual(user, self.user)
|
self.assertEqual(user, self.user)
|
||||||
|
|
||||||
|
""" Alt Login disabled
|
||||||
def test_authenticate_alt_character(self):
|
def test_authenticate_alt_character(self):
|
||||||
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
|
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
|
||||||
user = StateBackend().authenticate(token=t)
|
user = StateBackend().authenticate(token=t)
|
||||||
self.assertEqual(user, self.user)
|
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):
|
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')
|
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.username, 'Unclaimed_Character')
|
||||||
self.assertEqual(user.profile.main_character, self.unclaimed_character)
|
self.assertEqual(user.profile.main_character, self.unclaimed_character)
|
||||||
|
|
||||||
|
""" Alt Login disabled
|
||||||
def test_authenticate_character_record(self):
|
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')
|
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')
|
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.assertEqual(user, self.old_user)
|
||||||
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
||||||
self.assertTrue(user.profile.main_character)
|
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):
|
def test_iterate_username(self):
|
||||||
t = Token(character_id=self.unclaimed_character.character_id,
|
t = Token(character_id=self.unclaimed_character.character_id,
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ from urllib import parse
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.shortcuts import reverse
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
from django.urls import reverse, URLPattern
|
||||||
|
|
||||||
from allianceauth.eveonline.models import EveCharacter
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
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'
|
MODULE_PATH = 'allianceauth.authentication'
|
||||||
|
|
||||||
@@ -66,3 +66,33 @@ class DecoratorTestCase(TestCase):
|
|||||||
setattr(self.request, 'user', self.main_user)
|
setattr(self.request, 'user', self.main_user)
|
||||||
response = self.dummy_view(self.request)
|
response = self.dummy_view(self.request)
|
||||||
self.assertEqual(response.status_code, 200)
|
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,
|
views.add_character,
|
||||||
name='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'),
|
path('dashboard/', views.dashboard, name='dashboard'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -61,6 +61,44 @@ def dashboard(request):
|
|||||||
}
|
}
|
||||||
return render(request, 'authentication/dashboard.html', context)
|
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
|
@login_required
|
||||||
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ from .views import NightModeRedirectView
|
|||||||
def auth_settings(request):
|
def auth_settings(request):
|
||||||
return {
|
return {
|
||||||
'SITE_NAME': settings.SITE_NAME,
|
'SITE_NAME': settings.SITE_NAME,
|
||||||
|
'SITE_URL': settings.SITE_URL,
|
||||||
'NIGHT_MODE': NightModeRedirectView.night_mode_state(request),
|
'NIGHT_MODE': NightModeRedirectView.night_mode_state(request),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center col-lg-6{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}">
|
<td class="text-center col-lg-6{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}">
|
||||||
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}">
|
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}">
|
||||||
</td>
|
</td>
|
||||||
{% if corpstats.corp.alliance %}
|
{% if corpstats.corp.alliance %}
|
||||||
<td class="text-center col-lg-6">
|
<td class="text-center col-lg-6">
|
||||||
<img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}">
|
<img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}" alt="{{ corpstats.corp.alliance.alliance_name }}">
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-center" style="vertical-align:middle">
|
<td class="text-center" style="vertical-align:middle">
|
||||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||||
<img src="{{ main.main.portrait_url_64 }}" class="img-circle">
|
<img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
|
||||||
<div class="caption text-center">
|
<div class="caption text-center">
|
||||||
{{ main.main }}
|
{{ main.main }}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-center" style="width:5%">
|
<td class="text-center" style="width:5%">
|
||||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||||
<img src="{{ alt.portrait_url_32 }}" class="img-circle">
|
<img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
|
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for member in members %}
|
{% for member in members %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
||||||
<td class="text-center">{{ member }}</td>
|
<td class="text-center">{{ member }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a>
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for member in unregistered %}
|
{% for member in unregistered %}
|
||||||
<tr class="danger">
|
<tr class="danger">
|
||||||
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||||
<td class="text-center">{{ member.character_name }}</td>
|
<td class="text-center">{{ member.character_name }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a>
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for member in unregistered %}
|
{% for member in unregistered %}
|
||||||
<tr class="danger">
|
<tr class="danger">
|
||||||
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||||
<td class="text-center">{{ member.character_name }}</td>
|
<td class="text-center">{{ member.character_name }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for result in results %}
|
{% for result in results %}
|
||||||
<tr {% if not result.1.registered %}class="danger"{% endif %}>
|
<tr {% if not result.1.registered %}class="danger"{% endif %}>
|
||||||
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle"></td>
|
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
||||||
<td class="text-center">{{ result.1.character_name }}</td>
|
<td class="text-center">{{ result.1.character_name }}</td>
|
||||||
<td class="text-center">{{ result.0.corp.corporation_name }}</td>
|
<td class="text-center">{{ result.0.corp.corporation_name }}</td>
|
||||||
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ def sync_user_groups(modeladmin, request, queryset):
|
|||||||
agc.update_all_states_group_membership()
|
agc.update_all_states_group_membership()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(AutogroupsConfig)
|
||||||
class AutogroupsConfigAdmin(admin.ModelAdmin):
|
class AutogroupsConfigAdmin(admin.ModelAdmin):
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
models.CharField: {'strip': False}
|
models.CharField: {'strip': False}
|
||||||
@@ -36,6 +37,5 @@ class AutogroupsConfigAdmin(admin.ModelAdmin):
|
|||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)
|
|
||||||
admin.site.register(ManagedCorpGroup)
|
admin.site.register(ManagedCorpGroup)
|
||||||
admin.site.register(ManagedAllianceGroup)
|
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."""
|
"""An alliance in Eve Online."""
|
||||||
|
|
||||||
alliance_id = models.PositiveIntegerField(unique=True)
|
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)
|
alliance_ticker = models.CharField(max_length=254)
|
||||||
executor_corp_id = models.PositiveIntegerField()
|
executor_corp_id = models.PositiveIntegerField()
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ class EveCorporationInfo(models.Model):
|
|||||||
"""A corporation in Eve Online."""
|
"""A corporation in Eve Online."""
|
||||||
|
|
||||||
corporation_id = models.PositiveIntegerField(unique=True)
|
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)
|
corporation_ticker = models.CharField(max_length=254)
|
||||||
member_count = models.IntegerField()
|
member_count = models.IntegerField()
|
||||||
ceo_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
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 esi.clients import esi_client_factory
|
||||||
|
|
||||||
from allianceauth import __version__
|
from allianceauth import __version__
|
||||||
|
from allianceauth.utils.django import StartupCommand
|
||||||
|
|
||||||
|
|
||||||
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
|
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
|
||||||
@@ -175,15 +176,16 @@ class EveProvider:
|
|||||||
|
|
||||||
class EveSwaggerProvider(EveProvider):
|
class EveSwaggerProvider(EveProvider):
|
||||||
def __init__(self, token=None, adapter=None):
|
def __init__(self, token=None, adapter=None):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG or StartupCommand().is_management_command:
|
||||||
self._client = None
|
self._client = None
|
||||||
logger.info(
|
logger.info('ESI client will be loaded on-demand')
|
||||||
'DEBUG mode detected: ESI client will be loaded on-demand.'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
|
logger.info('Loading ESI client')
|
||||||
try:
|
try:
|
||||||
self._client = esi_client_factory(
|
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):
|
except (HTTPError, RefResolutionError):
|
||||||
logger.exception(
|
logger.exception(
|
||||||
|
|||||||
0
allianceauth/eveonline/views.py
Executable file → Normal file
0
allianceauth/eveonline/views.py
Executable file → Normal file
@@ -12,7 +12,7 @@
|
|||||||
<div class="panel-heading">{{ character_name }}</div>
|
<div class="panel-heading">{{ character_name }}</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="col-lg-2 col-sm-2">
|
<div class="col-lg-2 col-sm-2">
|
||||||
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}">
|
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}" alt="{{ character_name }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-10 col-sm-2">
|
<div class="col-lg-10 col-sm-2">
|
||||||
<div class="alert alert-danger" role="alert">{% translate "Character not registered!" %}</div>
|
<div class="alert alert-danger" role="alert">{% translate "Character not registered!" %}</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Create Fatlink" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Create Fatlink" %}{% endblock page_title %}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@
|
|||||||
<td class="text-center">{{ fat.user }}</td>
|
<td class="text-center">{{ fat.user }}</td>
|
||||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||||
{% if fat.station != "No Station" %}
|
{% 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 %}
|
{% else %}
|
||||||
<td class="text-center">{{ fat.system }}</td>
|
<td class="text-center">{{ fat.system }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-lg-12">
|
<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 %}
|
{% if char_id %}
|
||||||
<div class="text-right">
|
<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>
|
<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>
|
||||||
@@ -16,11 +14,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
<h2>
|
<h2>
|
||||||
{% blocktrans count links=n_fats trimmed %}
|
{% blocktranslate count links=n_fats trimmed %}
|
||||||
{{ user }} has collected one link this month.
|
{{ user }} has collected one link this month.
|
||||||
{% plural %}
|
{% plural %}
|
||||||
{{ user }} has collected {{ links }} links this month.
|
{{ user }} has collected {{ links }} links this month.
|
||||||
{% endblocktrans %}
|
{% endblocktranslate %}
|
||||||
</h2>
|
</h2>
|
||||||
<table class="table table-responsive">
|
<table class="table table-responsive">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -36,11 +34,11 @@
|
|||||||
</table>
|
</table>
|
||||||
{% if created_fats %}
|
{% if created_fats %}
|
||||||
<h2>
|
<h2>
|
||||||
{% blocktrans count links=n_created_fats trimmed %}
|
{% blocktranslate count links=n_created_fats trimmed %}
|
||||||
{{ user }} has created one link this month.
|
{{ user }} has created one link this month.
|
||||||
{% plural %}
|
{% plural %}
|
||||||
{{ user }} has created {{ links }} links this month.
|
{{ user }} has created {{ links }} links this month.
|
||||||
{% endblocktrans %}
|
{% endblocktranslate %}
|
||||||
</h2>
|
</h2>
|
||||||
{% if created_fats %}
|
{% if created_fats %}
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-lg-12">
|
<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">
|
<div class="text-right">
|
||||||
<a href="{% url 'fatlink:personal_statistics_year' previous_year %}" class="btn btn-info">{% translate "Previous year" %}</a>
|
<a href="{% url 'fatlink:personal_statistics_year' previous_year %}" class="btn btn-info">{% translate "Previous year" %}</a>
|
||||||
{% if next_year %}
|
{% if next_year %}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Fatlink Corp Statistics" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Fatlink Corp Statistics" %}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-lg-12">
|
<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">
|
<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>
|
<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 %}
|
{% if next_month %}
|
||||||
@@ -29,7 +27,7 @@
|
|||||||
{% for memberStat in fatStats %}
|
{% for memberStat in fatStats %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive">
|
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
|
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
|
||||||
<td class="text-center">{{ memberStat.n_chars }}</td>
|
<td class="text-center">{{ memberStat.n_chars }}</td>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Fatlink statistics" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Fatlink statistics" %}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-lg-12">
|
<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">
|
<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>
|
<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 %}
|
{% if next_month %}
|
||||||
@@ -30,9 +28,9 @@
|
|||||||
{% for corpStat in fatStats %}
|
{% for corpStat in fatStats %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive">
|
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center"><a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</td>
|
<td class="text-center"><a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a></td>
|
||||||
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
|
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
|
||||||
<td class="text-center">{{ corpStat.corp.member_count }}</td>
|
<td class="text-center">{{ corpStat.corp.member_count }}</td>
|
||||||
<td class="text-center">{{ corpStat.n_fats }}</td>
|
<td class="text-center">{{ corpStat.n_fats }}</td>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
|
||||||
@@ -35,7 +33,7 @@
|
|||||||
<td class="text-center">{{ fat.fatlink.fleet }}</td>
|
<td class="text-center">{{ fat.fatlink.fleet }}</td>
|
||||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||||
{% if fat.station != "No Station" %}
|
{% 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 %}
|
{% else %}
|
||||||
<td class="text-center">{{ fat.system }}</td>
|
<td class="text-center">{{ fat.system }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -248,59 +248,82 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@token_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):
|
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:
|
if location['station_id']:
|
||||||
# get data
|
location['station_name'] = \
|
||||||
c = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
|
||||||
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
|
elif location['structure_id']:
|
||||||
ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result()
|
location['station_name'] = \
|
||||||
location['solar_system_name'] = \
|
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
|
||||||
c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()['name']
|
'name']
|
||||||
if location['station_id']:
|
else:
|
||||||
location['station_name'] = \
|
location['station_name'] = "No Station"
|
||||||
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
|
|
||||||
elif location['structure_id']:
|
ship['ship_type_name'] = provider.get_itemtype(ship['ship_type_id']).name
|
||||||
location['station_name'] = \
|
|
||||||
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
|
fat = Fat()
|
||||||
'name']
|
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:
|
else:
|
||||||
location['station_name'] = "No Station"
|
context = {
|
||||||
ship['ship_type_name'] = provider.get_itemtype(ship['ship_type_id']).name
|
'character_id': token.character_id,
|
||||||
|
'character_name': token.character_name,
|
||||||
|
'character_portrait_url': EveCharacter.generic_portrait_url(
|
||||||
|
token.character_id, 128
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
fat = Fat()
|
return render(request, 'fleetactivitytracking/characternotexisting.html', context=context)
|
||||||
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:
|
else:
|
||||||
context = {
|
messages.error(request, _('FAT link has expired.'))
|
||||||
'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)
|
|
||||||
else:
|
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')
|
return redirect('fatlink:view')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{{ group }} {% translate "Audit Log" %}{% endblock page_title %}
|
{% block page_title %}{{ group }} {% translate "Audit Log" %}{% endblock page_title %}
|
||||||
@@ -25,13 +24,15 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped" id="log-entries">
|
<table class="table table-striped" id="log-entries">
|
||||||
<thead>
|
<thead>
|
||||||
<th scope="col">{% translate "Date/Time" %}</th>
|
<tr>
|
||||||
<th scope="col">{% translate "Requestor" %}</th>
|
<th scope="col">{% translate "Date/Time" %}</th>
|
||||||
<th scope="col">{% translate "Character" %}</th>
|
<th scope="col">{% translate "Requestor" %}</th>
|
||||||
<th scope="col">{% translate "Corporation" %}</th>
|
<th scope="col">{% translate "Character" %}</th>
|
||||||
<th scope="col">{% translate "Type" %}</th>
|
<th scope="col">{% translate "Corporation" %}</th>
|
||||||
<th scope="col">{% translate "Action" %}</th>
|
<th scope="col">{% translate "Type" %}</th>
|
||||||
<th scope="col">{% translate "Actor" %}</th>
|
<th scope="col">{% translate "Action" %}</th>
|
||||||
|
<th scope="col">{% translate "Actor" %}</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
{% block extra_javascript %}
|
{% block extra_javascript %}
|
||||||
{% include 'bundles/datatables-js.html' %}
|
{% include 'bundles/datatables-js.html' %}
|
||||||
{% include 'bundles/moment-js.html' with locale=True %}
|
{% include 'bundles/moment-js.html' with locale=True %}
|
||||||
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
{% include 'bundles/filterdropdown-js.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load evelinks %}
|
{% load evelinks %}
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@
|
|||||||
{% for member in members %}
|
{% for member in members %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ member.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
<img src="{{ member.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;" alt="{{ member.main_char.character_name }}">
|
||||||
{% if member.main_char %}
|
{% if member.main_char %}
|
||||||
<a href="{{ member.main_char|evewho_character_url }}" target="_blank">
|
<a href="{{ member.main_char|evewho_character_url }}" target="_blank">
|
||||||
{{ member.main_char.character_name }}
|
{{ member.main_char.character_name }}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %}
|
||||||
@@ -61,7 +60,7 @@
|
|||||||
<i class="glyphicon glyphicon-list-alt"></i>
|
<i class="glyphicon glyphicon-list-alt"></i>
|
||||||
</a>
|
</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>
|
<i class="glyphicon glyphicon-copy"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Available Groups" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Available Groups" %}{% endblock page_title %}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load evelinks %}
|
{% load evelinks %}
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
{% for acceptrequest in acceptrequests %}
|
{% for acceptrequest in acceptrequests %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;" alt="{{ acceptrequest.main_char.character_name }}">
|
||||||
{% if acceptrequest.main_char %}
|
{% if acceptrequest.main_char %}
|
||||||
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
|
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
|
||||||
{{ acceptrequest.main_char.character_name }}
|
{{ acceptrequest.main_char.character_name }}
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
{% for leaverequest in leaverequests %}
|
{% for leaverequest in leaverequests %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;" alt="{{ leaverequest.main_char.character_name }}">
|
||||||
{% if leaverequest.main_char %}
|
{% if leaverequest.main_char %}
|
||||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||||
{{ leaverequest.main_char.character_name }}
|
{{ leaverequest.main_char.character_name }}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load navactive %}
|
{% load navactive %}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ app_name = "groupmanagement"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# groups
|
# 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/join/<int:group_id>/", views.group_request_add, name="request_add"),
|
||||||
path(
|
path(
|
||||||
"group/request/leave/<int:group_id>/", views.group_request_leave, name="request_leave"
|
"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_plural = 'Choices (optional)'
|
||||||
verbose_name= 'Choice'
|
verbose_name= 'Choice'
|
||||||
|
|
||||||
|
@admin.register(ApplicationQuestion)
|
||||||
class QuestionAdmin(admin.ModelAdmin):
|
class QuestionAdmin(admin.ModelAdmin):
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {'fields': ['title', 'help_text', 'multi_select']}),
|
(None, {'fields': ['title', 'help_text', 'multi_select']}),
|
||||||
@@ -18,6 +19,5 @@ class QuestionAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
admin.site.register(Application)
|
admin.site.register(Application)
|
||||||
admin.site.register(ApplicationComment)
|
admin.site.register(ApplicationComment)
|
||||||
admin.site.register(ApplicationQuestion, QuestionAdmin)
|
|
||||||
admin.site.register(ApplicationForm)
|
admin.site.register(ApplicationForm)
|
||||||
admin.site.register(ApplicationResponse)
|
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
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %}
|
{% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}
|
{% block page_title %}{% translate "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}
|
||||||
@@ -16,11 +15,11 @@
|
|||||||
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
|
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
{% if question.help_text %}
|
{% if question.help_text %}
|
||||||
<div cass="text-center">{{ question.help_text }}</div>
|
<div class="text-center">{{ question.help_text }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for choice in question.choices.all %}
|
{% for choice in question.choices.all %}
|
||||||
<input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}">
|
<input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}_choice_{{ forloop.counter }}" value="{{ choice.choice_text }}">
|
||||||
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
|
<label for="id_{{ question.pk }}_choice_{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
|
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}
|
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}
|
||||||
@@ -178,7 +177,7 @@
|
|||||||
<h4 class="modal-title" id="myModalLabel">{% translate "Application Search" %}</h4>
|
<h4 class="modal-title" id="myModalLabel">{% translate "Application Search" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
|
<form class="form-signin" role="form" action="{% url 'hrapplications:search' %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ search_form|bootstrap }}
|
{{ search_form|bootstrap }}
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}
|
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
<h4 class="modal-title" id="myModalLabel">{% translate "Application Search" %}</h4>
|
<h4 class="modal-title" id="myModalLabel">{% translate "Application Search" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
|
<form class="form-signin" role="form" action="{% url 'hrapplications:search' %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ search_form|bootstrap }}
|
{{ search_form|bootstrap }}
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
{% for char in app.characters %}
|
{% for char in app.characters %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<img class="ra-avatar img-responsive img-circle" src="{{ char.portrait_url_32 }}">
|
<img class="ra-avatar img-responsive img-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ char.character_name }}</td>
|
<td class="text-center">{{ char.character_name }}</td>
|
||||||
<td class="text-center">{{ char.corporation_name }}</td>
|
<td class="text-center">{{ char.corporation_name }}</td>
|
||||||
|
|||||||
20
allianceauth/hrapplications/views.py
Executable file → Normal file
20
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)
|
app_form = get_object_or_404(ApplicationForm, id=form_id)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if Application.objects.filter(user=request.user).filter(form=app_form).exists():
|
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:
|
else:
|
||||||
application = Application(user=request.user, form=app_form)
|
application = Application(user=request.user, form=app_form)
|
||||||
application.save()
|
application.save()
|
||||||
@@ -92,7 +92,7 @@ def hr_application_personal_view(request, app_id):
|
|||||||
}
|
}
|
||||||
return render(request, 'hrapplications/view.html', context=context)
|
return render(request, 'hrapplications/view.html', context=context)
|
||||||
else:
|
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')
|
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}")
|
logger.info(f"User {request.user} deleting {app}")
|
||||||
app.delete()
|
app.delete()
|
||||||
else:
|
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:
|
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')
|
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}")
|
logger.info(f"Saved comment by user {request.user} to {app}")
|
||||||
return redirect('hrapplications:view', app_id)
|
return redirect('hrapplications:view', app_id)
|
||||||
else:
|
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)
|
return redirect('hrapplications:view', app_id)
|
||||||
else:
|
else:
|
||||||
logger.debug("Returning blank HRApplication comment form.")
|
logger.debug("Returning blank HRApplication comment form.")
|
||||||
@@ -171,7 +171,7 @@ def hr_application_approve(request, app_id):
|
|||||||
app.save()
|
app.save()
|
||||||
notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success")
|
notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success")
|
||||||
else:
|
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')
|
return redirect('hrapplications:index')
|
||||||
|
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ def hr_application_reject(request, app_id):
|
|||||||
app.save()
|
app.save()
|
||||||
notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger")
|
notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger")
|
||||||
else:
|
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')
|
return redirect('hrapplications:index')
|
||||||
|
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ def hr_application_search(request):
|
|||||||
app_list = app_list.filter(
|
app_list = app_list.filter(
|
||||||
form__corp__corporation_id=request.user.profile.main_character.corporation_id)
|
form__corp__corporation_id=request.user.profile.main_character.corporation_id)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
"User %s missing main character model: unable to filter applications to search" % request.user)
|
"User %s missing main character model: unable to filter applications to search" % request.user)
|
||||||
|
|
||||||
applications = app_list.filter(
|
applications = app_list.filter(
|
||||||
@@ -219,7 +219,7 @@ def hr_application_search(request):
|
|||||||
Q(user__character_ownerships__character__corporation_name__icontains=searchstring) |
|
Q(user__character_ownerships__character__corporation_name__icontains=searchstring) |
|
||||||
Q(user__character_ownerships__character__alliance_name__icontains=searchstring) |
|
Q(user__character_ownerships__character__alliance_name__icontains=searchstring) |
|
||||||
Q(user__username__icontains=searchstring)
|
Q(user__username__icontains=searchstring)
|
||||||
)
|
).distinct()
|
||||||
|
|
||||||
context = {'applications': applications, 'search_form': HRApplicationSearchForm()}
|
context = {'applications': applications, 'search_form': HRApplicationSearchForm()}
|
||||||
|
|
||||||
@@ -246,6 +246,6 @@ def hr_application_mark_in_progress(request, app_id):
|
|||||||
app.save()
|
app.save()
|
||||||
notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}")
|
notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}")
|
||||||
else:
|
else:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}")
|
f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}")
|
||||||
return redirect("hrapplications:view", app_id)
|
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
|
from .core import notify # noqa: F401
|
||||||
|
|
||||||
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
|
|
||||||
|
|||||||
@@ -15,18 +15,22 @@ class NotificationAdmin(admin.ModelAdmin):
|
|||||||
ordering = ("-timestamp", )
|
ordering = ("-timestamp", )
|
||||||
search_fields = ["user__username", "user__profile__main_character__character_name"]
|
search_fields = ["user__username", "user__profile__main_character__character_name"]
|
||||||
|
|
||||||
|
@admin.display(
|
||||||
|
ordering="user__profile__main_character__character_name"
|
||||||
|
)
|
||||||
def _main(self, obj):
|
def _main(self, obj):
|
||||||
try:
|
try:
|
||||||
return obj.user.profile.main_character
|
return obj.user.profile.main_character
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return obj.user
|
return obj.user
|
||||||
|
|
||||||
_main.admin_order_field = "user__profile__main_character__character_name"
|
|
||||||
|
|
||||||
|
@admin.display(
|
||||||
|
ordering="user__profile__state__name"
|
||||||
|
)
|
||||||
def _state(self, obj):
|
def _state(self, obj):
|
||||||
return obj.user.profile.state
|
return obj.user.profile.state
|
||||||
|
|
||||||
_state.admin_order_field = "user__profile__state__name"
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user