mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 14:16:21 +01:00
Compare commits
359 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
054ef27fa4 | ||
|
|
97e224b8e6 | ||
|
|
3b8fa415bc | ||
|
|
b94fd7ed19 | ||
|
|
d1dac61135 | ||
|
|
d2a095217f | ||
|
|
5e9b47cf79 | ||
|
|
853826c140 | ||
|
|
9c7de58989 | ||
|
|
3de988369f | ||
|
|
6e3219fd1b | ||
|
|
8aeb061635 | ||
|
|
84e2107b62 | ||
|
|
20fcf5efa4 | ||
|
|
c15b955d5e | ||
|
|
65e1545a66 | ||
|
|
c558a980e1 | ||
|
|
bd8ef84862 | ||
|
|
42e96d2f14 | ||
|
|
23a3dd1ab9 | ||
|
|
81e5bc5337 | ||
|
|
a8ef844fe7 | ||
|
|
9ce1939040 | ||
|
|
322131cd4f | ||
|
|
55e6e92da5 | ||
|
|
e5d29629a5 | ||
|
|
26e187e4c8 | ||
|
|
3480c4e0e8 | ||
|
|
1544f097e0 | ||
|
|
2477c31656 | ||
|
|
0dc631d69e | ||
|
|
2a9981cdb9 | ||
|
|
004c48b8ad | ||
|
|
4d66b7d456 | ||
|
|
77e5747a23 | ||
|
|
6118c0ddec | ||
|
|
ce25deeca1 | ||
|
|
60084de3db | ||
|
|
e16c68e255 | ||
|
|
bf14e9c7c3 | ||
|
|
98e91fe207 | ||
|
|
7024552c4e | ||
|
|
a0719e4b86 | ||
|
|
906c589f14 | ||
|
|
ffb526ab0c | ||
|
|
b9d128259e | ||
|
|
13d866bd0d | ||
|
|
ea1887b9ec | ||
|
|
d2f8c2a42f | ||
|
|
424246df26 | ||
|
|
563e2210ef | ||
|
|
02a1078005 | ||
|
|
30107de44e | ||
|
|
200e8f2ff1 | ||
|
|
77a08cd218 | ||
|
|
e5a09027e5 | ||
|
|
52b6c5d341 | ||
|
|
8b895b76b5 | ||
|
|
babd71702f | ||
|
|
3ec3cbdff7 | ||
|
|
51611e1237 | ||
|
|
39519bab91 | ||
|
|
90dc6a4d4c | ||
|
|
53ffd7f885 | ||
|
|
efc7475228 | ||
|
|
380c41400b | ||
|
|
079c12a72e | ||
|
|
4f1ebedc44 | ||
|
|
66822107e3 | ||
|
|
7856cd5ce4 | ||
|
|
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 | ||
|
|
7f8ca4fad2 | ||
|
|
4bb9a7155d | ||
|
|
2ac79954f3 | ||
|
|
585e1f47f3 | ||
|
|
a33c474b35 | ||
|
|
61c3d8964b | ||
|
|
1c927c5820 | ||
|
|
ff0fa0329d | ||
|
|
e51ea439ca | ||
|
|
113977b19f | ||
|
|
8f39b50b6d | ||
|
|
95b309c358 | ||
|
|
cf3df3b715 | ||
|
|
d815028c4d | ||
|
|
ac5570abe2 | ||
|
|
84ad571aa4 | ||
|
|
38e7705ae7 | ||
|
|
0b6af014fa | ||
|
|
2401f2299d | ||
|
|
919768c8bb | ||
|
|
24db21463b | ||
|
|
1e029af83a | ||
|
|
53dd8ce606 | ||
|
|
0f4003366d | ||
|
|
2b31be789d | ||
|
|
bf1b4bb549 | ||
|
|
dd42b807f0 | ||
|
|
542fbafd98 | ||
|
|
37b9f5c882 | ||
|
|
5bde9a6952 |
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
|
|
||||||
|
|||||||
102
.gitlab-ci.yml
102
.gitlab-ci.yml
@@ -5,16 +5,16 @@
|
|||||||
- merge_requests
|
- merge_requests
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
- gitlab
|
- gitlab
|
||||||
- test
|
- test
|
||||||
- deploy
|
- deploy
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- template: Dependency-Scanning.gitlab-ci.yml
|
- template: Dependency-Scanning.gitlab-ci.yml
|
||||||
- template: Security/SAST.gitlab-ci.yml
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install redis-server -y
|
- apt-get update && apt-get install redis-server -y
|
||||||
@@ -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:
|
||||||
@@ -42,16 +42,20 @@ sast:
|
|||||||
dependency_scanning:
|
dependency_scanning:
|
||||||
stage: gitlab
|
stage: gitlab
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install redis-server libmariadb-dev -y
|
- apt-get update && apt-get install redis-server libmariadb-dev -y
|
||||||
- redis-server --daemonize yes
|
- redis-server --daemonize yes
|
||||||
- python -V
|
- python -V
|
||||||
- pip install wheel tox
|
- pip install wheel tox
|
||||||
|
|
||||||
|
secret_detection:
|
||||||
|
stage: gitlab
|
||||||
|
before_script: []
|
||||||
|
|
||||||
test-3.8-core:
|
test-3.8-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.8-bullseye
|
image: python:3.8-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py38-core
|
- tox -e py38-core
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -63,7 +67,7 @@ test-3.9-core:
|
|||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.9-bullseye
|
image: python:3.9-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py39-core
|
- tox -e py39-core
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -75,7 +79,7 @@ test-3.10-core:
|
|||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.10-bullseye
|
image: python:3.10-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py310-core
|
- tox -e py310-core
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -85,9 +89,21 @@ 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:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-pvpy-core:
|
||||||
|
<<: *only-default
|
||||||
|
image: pypy:3.9-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e pypy-all
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -100,7 +116,7 @@ test-3.8-all:
|
|||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.8-bullseye
|
image: python:3.8-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py38-all
|
- tox -e py38-all
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -112,7 +128,7 @@ test-3.9-all:
|
|||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.9-bullseye
|
image: python:3.9-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py39-all
|
- tox -e py39-all
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -124,7 +140,7 @@ test-3.10-all:
|
|||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.10-bullseye
|
image: python:3.10-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py310-all
|
- tox -e py310-all
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -134,9 +150,22 @@ 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:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||||
|
|
||||||
|
test-pvpy-all:
|
||||||
|
<<: *only-default
|
||||||
|
image: pypy:3.9-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e pypy-all
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
@@ -145,6 +174,31 @@ test-3.11-all:
|
|||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
|
build-test:
|
||||||
|
stage: test
|
||||||
|
image: python:3.10-bullseye
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- python -m pip install --upgrade pip
|
||||||
|
- python -m pip install --upgrade build
|
||||||
|
- python -m pip install --upgrade setuptools wheel
|
||||||
|
|
||||||
|
script:
|
||||||
|
- python -m build
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
|
||||||
|
paths:
|
||||||
|
- dist/*
|
||||||
|
expire_in: 1 year
|
||||||
|
|
||||||
|
test-docs:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.11-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e docs
|
||||||
|
|
||||||
deploy_production:
|
deploy_production:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: python:3.10-bullseye
|
image: python:3.10-bullseye
|
||||||
@@ -184,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.1.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.31.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.0
|
|
||||||
hooks:
|
|
||||||
- id: setup-cfg-fmt
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ version: 2
|
|||||||
|
|
||||||
# Set the version of Python and other tools you might need
|
# Set the version of Python and other tools you might need
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-22.04
|
||||||
apt_packages:
|
apt_packages:
|
||||||
- redis
|
- redis
|
||||||
tools:
|
tools:
|
||||||
python: "3.10"
|
python: "3.11"
|
||||||
|
|
||||||
# Build documentation in the docs/ directory with Sphinx
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
sphinx:
|
sphinx:
|
||||||
@@ -20,7 +20,10 @@ sphinx:
|
|||||||
# Optionally build your docs in additional formats such as PDF and ePub
|
# Optionally build your docs in additional formats such as PDF and ePub
|
||||||
formats: all
|
formats: all
|
||||||
|
|
||||||
# Optionally set the version of Python and requirements required to build your docs
|
# Python requirements required to build your docs
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
- requirements: docs/requirements.txt
|
- method: pip
|
||||||
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- docs
|
||||||
|
|||||||
10
.tx/config
Normal file
10
.tx/config
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[main]
|
||||||
|
host = https://app.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/transifex.yml
Normal file
10
.tx/transifex.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
filters:
|
||||||
|
- filter_type: file
|
||||||
|
file_format: PO
|
||||||
|
source_file: allianceauth/locale/en/LC_MESSAGES/django.po
|
||||||
|
source_language: en
|
||||||
|
translation_files_expression: allianceauth/locale/<lang>/LC_MESSAGES/django.po
|
||||||
|
|
||||||
|
settings:
|
||||||
|
language_mapping:
|
||||||
|
zh-Hans: zh_Hans
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
include LICENSE
|
|
||||||
include README.md
|
|
||||||
include MANIFEST.in
|
|
||||||
graft allianceauth
|
|
||||||
|
|
||||||
global-exclude __pycache__
|
|
||||||
global-exclude *.py[co]
|
|
||||||
8
README.md
Executable file → Normal file
8
README.md
Executable file → Normal file
@@ -17,7 +17,7 @@ An auth system for EVE Online to help in-game organizations manage online servic
|
|||||||
- [Documentation](http://allianceauth.rtfd.io)
|
- [Documentation](http://allianceauth.rtfd.io)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
|
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
|
||||||
- [Developer Team](#developer-team)
|
- [Developer Team](#development-team)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -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.0a5'
|
__version__ = '3.8.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):
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ 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.
|
||||||
|
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 +61,18 @@ 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 user.profile.main_character.character_id != token.character_id:
|
||||||
|
# this is an alt, enforce main only due to trust issues in SSO.
|
||||||
|
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)
|
||||||
|
|||||||
0
allianceauth/authentication/core/__init__.py
Normal file
0
allianceauth/authentication/core/__init__.py
Normal file
48
allianceauth/authentication/core/celery_workers.py
Normal file
48
allianceauth/authentication/core/celery_workers.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""API for interacting with celery workers."""
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from amqp.exceptions import ChannelError
|
||||||
|
from celery import current_app
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def active_tasks_count() -> Optional[int]:
|
||||||
|
"""Return count of currently active tasks
|
||||||
|
or None if celery workers are not online.
|
||||||
|
"""
|
||||||
|
inspect = current_app.control.inspect()
|
||||||
|
return _tasks_count(inspect.active())
|
||||||
|
|
||||||
|
|
||||||
|
def _tasks_count(data: dict) -> Optional[int]:
|
||||||
|
"""Return count of tasks in data from celery inspect API."""
|
||||||
|
try:
|
||||||
|
tasks = itertools.chain(*data.values())
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
return len(list(tasks))
|
||||||
|
|
||||||
|
|
||||||
|
def queued_tasks_count() -> Optional[int]:
|
||||||
|
"""Return count of queued tasks. Return None if there was an error."""
|
||||||
|
try:
|
||||||
|
with current_app.connection_or_acquire() as conn:
|
||||||
|
result = conn.default_channel.queue_declare(
|
||||||
|
queue=getattr(settings, "CELERY_DEFAULT_QUEUE", "celery"), passive=True
|
||||||
|
)
|
||||||
|
return result.message_count
|
||||||
|
|
||||||
|
except ChannelError:
|
||||||
|
# Queue doesn't exist, probably empty
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to get celery queue length")
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -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,29 +1,34 @@
|
|||||||
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
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
|
||||||
_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
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
|
"""Event series for Task Statistics."""
|
||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from typing import Optional, List
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from redis import Redis
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
|
from redis import Redis
|
||||||
|
|
||||||
from django_redis import get_redis_connection
|
from .helpers import get_redis_client_or_stub
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EventSeries:
|
class EventSeries:
|
||||||
"""API for recording and analysing 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
|
||||||
if not isinstance(self._redis, Redis):
|
|
||||||
raise TypeError(
|
|
||||||
"This class requires a Redis client, but none was provided "
|
|
||||||
"and the default Django cache backend is not Redis either."
|
|
||||||
)
|
|
||||||
self._key_id = str(key_id)
|
self._key_id = str(key_id)
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_disabled(self):
|
||||||
|
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
||||||
|
return hasattr(self._redis, "IS_STUB")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _key_counter(self):
|
def _key_counter(self):
|
||||||
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
||||||
@@ -38,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."""
|
||||||
@@ -60,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.
|
||||||
@@ -76,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,
|
||||||
|
|||||||
49
allianceauth/authentication/task_statistics/helpers.py
Normal file
49
allianceauth/authentication/task_statistics/helpers.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""Helpers for Task Statistics."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from redis import Redis, RedisError
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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,9 +1,7 @@
|
|||||||
|
"""Signals for Task Statistics."""
|
||||||
|
|
||||||
from celery.signals import (
|
from celery.signals import (
|
||||||
task_failure,
|
task_failure, task_internal_error, task_retry, task_success, worker_ready,
|
||||||
task_internal_error,
|
|
||||||
task_retry,
|
|
||||||
task_success,
|
|
||||||
worker_ready
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -19,6 +17,7 @@ def reset_counters():
|
|||||||
|
|
||||||
|
|
||||||
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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,29 +4,30 @@ from django.test import TestCase
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from allianceauth.authentication.task_statistics.counters import (
|
from allianceauth.authentication.task_statistics.counters import (
|
||||||
dashboard_results,
|
dashboard_results, failed_tasks, retried_tasks, succeeded_tasks,
|
||||||
succeeded_tasks,
|
|
||||||
retried_tasks,
|
|
||||||
failed_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()
|
||||||
|
|
||||||
# when
|
# when
|
||||||
results = dashboard_results(hours=1)
|
results = dashboard_results(hours=1)
|
||||||
# then
|
# then
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
|
|
||||||
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 EventSeries
|
from allianceauth.authentication.task_statistics.event_series import (
|
||||||
|
EventSeries,
|
||||||
|
)
|
||||||
|
from allianceauth.authentication.task_statistics.helpers import _RedisStub
|
||||||
|
|
||||||
|
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
||||||
|
|
||||||
|
|
||||||
class TestEventSeries(TestCase):
|
class TestEventSeries(TestCase):
|
||||||
@@ -131,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,28 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from redis import RedisError
|
||||||
|
|
||||||
|
from allianceauth.authentication.task_statistics.helpers import (
|
||||||
|
_RedisStub, get_redis_client_or_stub,
|
||||||
|
)
|
||||||
|
|
||||||
|
MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"
|
||||||
|
|
||||||
|
|
||||||
|
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>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||||
{% 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 lang="{{ language.code }}" 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 %}
|
|
||||||
0
allianceauth/authentication/tests/core/__init__.py
Normal file
0
allianceauth/authentication/tests/core/__init__.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from amqp.exceptions import ChannelError
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.authentication.core.celery_workers import (
|
||||||
|
active_tasks_count, queued_tasks_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
MODULE_PATH = "allianceauth.authentication.core.celery_workers"
|
||||||
|
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + ".current_app")
|
||||||
|
class TestActiveTasksCount(TestCase):
|
||||||
|
def test_should_return_correct_count_when_no_active_tasks(self, mock_current_app):
|
||||||
|
# given
|
||||||
|
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||||
|
"queue": []
|
||||||
|
}
|
||||||
|
# when
|
||||||
|
result = active_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
|
||||||
|
def test_should_return_correct_task_count_for_active_tasks(self, mock_current_app):
|
||||||
|
# given
|
||||||
|
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||||
|
"queue": [1, 2, 3]
|
||||||
|
}
|
||||||
|
# when
|
||||||
|
result = active_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 3)
|
||||||
|
|
||||||
|
def test_should_return_correct_task_count_for_multiple_queues(
|
||||||
|
self, mock_current_app
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||||
|
"queue_1": [1, 2],
|
||||||
|
"queue_2": [3, 4],
|
||||||
|
}
|
||||||
|
# when
|
||||||
|
result = active_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 4)
|
||||||
|
|
||||||
|
def test_should_return_none_when_celery_not_available(self, mock_current_app):
|
||||||
|
# given
|
||||||
|
mock_current_app.control.inspect.return_value.active.return_value = None
|
||||||
|
# when
|
||||||
|
result = active_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + ".current_app")
|
||||||
|
class TestQueuedTasksCount(TestCase):
|
||||||
|
def test_should_return_queue_length_when_queue_exists(self, mock_current_app):
|
||||||
|
# given
|
||||||
|
mock_conn = (
|
||||||
|
mock_current_app.connection_or_acquire.return_value.__enter__.return_value
|
||||||
|
)
|
||||||
|
mock_conn.default_channel.queue_declare.return_value.message_count = 7
|
||||||
|
# when
|
||||||
|
result = queued_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 7)
|
||||||
|
|
||||||
|
def test_should_return_0_when_queue_does_not_exists(self, mock_current_app):
|
||||||
|
# given
|
||||||
|
mock_current_app.connection_or_acquire.side_effect = ChannelError
|
||||||
|
# when
|
||||||
|
result = queued_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
|
||||||
|
def test_should_return_None_on_other_errors(self, mock_current_app):
|
||||||
|
# given
|
||||||
|
mock_current_app.connection_or_acquire.side_effect = RuntimeError
|
||||||
|
# when
|
||||||
|
result = queued_tasks_count()
|
||||||
|
# then
|
||||||
|
self.assertIsNone(result)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -9,12 +9,8 @@ from django.core.cache import cache
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from allianceauth.templatetags.admin_status import (
|
from allianceauth.templatetags.admin_status import (
|
||||||
status_overview,
|
_current_notifications, _current_version_summary, _fetch_list_from_gitlab,
|
||||||
_fetch_list_from_gitlab,
|
_fetch_notification_issues_from_gitlab, _latests_versions, status_overview,
|
||||||
_current_notifications,
|
|
||||||
_current_version_summary,
|
|
||||||
_fetch_notification_issues_from_gitlab,
|
|
||||||
_latests_versions
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MODULE_PATH = 'allianceauth.templatetags'
|
MODULE_PATH = 'allianceauth.templatetags'
|
||||||
@@ -56,14 +52,10 @@ TEST_VERSION = '2.6.5'
|
|||||||
|
|
||||||
class TestStatusOverviewTag(TestCase):
|
class TestStatusOverviewTag(TestCase):
|
||||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||||
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
|
|
||||||
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
||||||
@patch(MODULE_PATH + '.admin_status._current_notifications')
|
@patch(MODULE_PATH + '.admin_status._current_notifications')
|
||||||
def test_status_overview(
|
def test_status_overview(
|
||||||
self,
|
self, mock_current_notifications, mock_current_version_info
|
||||||
mock_current_notifications,
|
|
||||||
mock_current_version_info,
|
|
||||||
mock_fetch_celery_queue_length
|
|
||||||
):
|
):
|
||||||
# given
|
# given
|
||||||
notifications = {
|
notifications = {
|
||||||
@@ -82,7 +74,6 @@ class TestStatusOverviewTag(TestCase):
|
|||||||
'latest_beta_version': '2.4.4a1',
|
'latest_beta_version': '2.4.4a1',
|
||||||
}
|
}
|
||||||
mock_current_version_info.return_value = version_info
|
mock_current_version_info.return_value = version_info
|
||||||
mock_fetch_celery_queue_length.return_value = 3
|
|
||||||
# when
|
# when
|
||||||
result = status_overview()
|
result = status_overview()
|
||||||
# then
|
# then
|
||||||
@@ -96,7 +87,6 @@ class TestStatusOverviewTag(TestCase):
|
|||||||
self.assertEqual(result["latest_minor_version"], '2.4.0')
|
self.assertEqual(result["latest_minor_version"], '2.4.0')
|
||||||
self.assertEqual(result["latest_patch_version"], '2.4.5')
|
self.assertEqual(result["latest_patch_version"], '2.4.5')
|
||||||
self.assertEqual(result["latest_beta_version"], '2.4.4a1')
|
self.assertEqual(result["latest_beta_version"], '2.4.4a1')
|
||||||
self.assertEqual(result["task_queue_length"], 3)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNotifications(TestCase):
|
class TestNotifications(TestCase):
|
||||||
|
|||||||
39
allianceauth/authentication/tests/test_views.py
Normal file
39
allianceauth/authentication/tests/test_views.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
|
from allianceauth.authentication.views import task_counts
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
MODULE_PATH = "allianceauth.authentication.views"
|
||||||
|
|
||||||
|
|
||||||
|
def jsonresponse_to_dict(response) -> dict:
|
||||||
|
return json.loads(response.content)
|
||||||
|
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + ".queued_tasks_count")
|
||||||
|
@patch(MODULE_PATH + ".active_tasks_count")
|
||||||
|
class TestRunningTasksCount(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
super().setUpClass()
|
||||||
|
cls.factory = RequestFactory()
|
||||||
|
cls.user = AuthUtils.create_user("bruce_wayne")
|
||||||
|
|
||||||
|
def test_should_return_data(
|
||||||
|
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
mock_active_tasks_count.return_value = 2
|
||||||
|
mock_queued_tasks_count.return_value = 3
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = self.user
|
||||||
|
# when
|
||||||
|
response = task_counts(request)
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertDictEqual(
|
||||||
|
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
|
||||||
|
)
|
||||||
@@ -22,5 +22,21 @@ 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'),
|
||||||
|
path('task-counts/', views.task_counts, name='task_counts'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django_registration.backends.activation.views import (
|
||||||
|
REGISTRATION_SALT, ActivationView as BaseActivationView,
|
||||||
|
RegistrationView as BaseRegistrationView,
|
||||||
|
)
|
||||||
|
from django_registration.signals import user_registered
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login, authenticate
|
from django.contrib.auth import authenticate, login
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.core.mail import EmailMultiAlternatives
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth.eveonline.models import EveCharacter
|
|
||||||
from esi.decorators import token_required
|
from esi.decorators import token_required
|
||||||
from esi.models import Token
|
from esi.models import Token
|
||||||
|
|
||||||
from django_registration.backends.activation.views import (
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
RegistrationView as BaseRegistrationView,
|
|
||||||
ActivationView as BaseActivationView,
|
|
||||||
REGISTRATION_SALT
|
|
||||||
)
|
|
||||||
from django_registration.signals import user_registered
|
|
||||||
|
|
||||||
from .models import CharacterOwnership
|
from .core.celery_workers import active_tasks_count, queued_tasks_count
|
||||||
from .forms import RegistrationForm
|
from .forms import RegistrationForm
|
||||||
|
from .models import CharacterOwnership
|
||||||
|
|
||||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||||
_has_auto_groups = True
|
_has_auto_groups = True
|
||||||
@@ -62,6 +62,47 @@ 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)
|
||||||
def main_character_change(request, token):
|
def main_character_change(request, token):
|
||||||
@@ -89,7 +130,7 @@ def main_character_change(request, token):
|
|||||||
def add_character(request, token):
|
def add_character(request, token):
|
||||||
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
||||||
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
||||||
messages.success(request, _('Added %(name)s to your account.'% ({'name': token.character_name})))
|
messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name})))
|
||||||
else:
|
else:
|
||||||
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
|
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
|
||||||
return redirect('authentication:dashboard')
|
return redirect('authentication:dashboard')
|
||||||
@@ -130,7 +171,13 @@ def sso_login(request, token):
|
|||||||
request.session['registration_uid'] = user.pk
|
request.session['registration_uid'] = user.pk
|
||||||
# Go to Step 2
|
# Go to Step 2
|
||||||
return redirect('registration_register')
|
return redirect('registration_register')
|
||||||
messages.error(request, _('Unable to authenticate as the selected character.'))
|
# Logging in with an alt is not allowed due to security concerns.
|
||||||
|
token.delete()
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_('Unable to authenticate as the selected character. '
|
||||||
|
'Please log in with the main character associated with this account.')
|
||||||
|
)
|
||||||
return redirect(settings.LOGIN_URL)
|
return redirect(settings.LOGIN_URL)
|
||||||
|
|
||||||
|
|
||||||
@@ -230,8 +277,11 @@ class ActivationView(BaseActivationView):
|
|||||||
|
|
||||||
def validate_key(self, activation_key):
|
def validate_key(self, activation_key):
|
||||||
try:
|
try:
|
||||||
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
|
dump = signing.loads(
|
||||||
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
|
activation_key,
|
||||||
|
salt=REGISTRATION_SALT,
|
||||||
|
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400
|
||||||
|
)
|
||||||
return dump
|
return dump
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
return None
|
return None
|
||||||
@@ -261,3 +311,12 @@ def activation_complete(request):
|
|||||||
def registration_closed(request):
|
def registration_closed(request):
|
||||||
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
|
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
|
||||||
return redirect('authentication:login')
|
return redirect('authentication:login')
|
||||||
|
|
||||||
|
|
||||||
|
def task_counts(request) -> JsonResponse:
|
||||||
|
"""Return task counts as JSON for an AJAX call."""
|
||||||
|
data = {
|
||||||
|
"tasks_running": active_tasks_count(),
|
||||||
|
"tasks_queued": queued_tasks_count()
|
||||||
|
}
|
||||||
|
return JsonResponse(data)
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -212,7 +212,14 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
|
|||||||
start_of_previous_month = first_day_of_previous_month(year, month)
|
start_of_previous_month = first_day_of_previous_month(year, month)
|
||||||
|
|
||||||
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
|
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
|
||||||
user = EveCharacter.objects.get(character_id=char_id).user
|
try:
|
||||||
|
user = EveCharacter.objects.get(character_id=char_id).character_ownership.user
|
||||||
|
except EveCharacter.DoesNotExist:
|
||||||
|
messages.error(request, _('Character does not exist'))
|
||||||
|
return redirect('fatlink:view')
|
||||||
|
except AttributeError:
|
||||||
|
messages.error(request, _('User does not exist'))
|
||||||
|
return redirect('fatlink:view')
|
||||||
else:
|
else:
|
||||||
user = request.user
|
user = request.user
|
||||||
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")
|
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")
|
||||||
@@ -241,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,8 +1,8 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import Group as BaseGroup
|
|
||||||
from django.contrib.auth.models import Permission, User
|
from django.contrib.auth.models import Group as BaseGroup, Permission, User
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Exists, OuterRef
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.db.models.signals import (
|
from django.db.models.signals import (
|
||||||
m2m_changed,
|
m2m_changed,
|
||||||
@@ -15,6 +15,7 @@ from django.dispatch import receiver
|
|||||||
|
|
||||||
from .forms import GroupAdminForm, ReservedGroupNameAdminForm
|
from .forms import GroupAdminForm, ReservedGroupNameAdminForm
|
||||||
from .models import AuthGroup, GroupRequest, ReservedGroupName
|
from .models import AuthGroup, GroupRequest, ReservedGroupName
|
||||||
|
from .tasks import remove_users_not_matching_states_from_group
|
||||||
|
|
||||||
if 'eve_autogroups' in apps.app_configs:
|
if 'eve_autogroups' in apps.app_configs:
|
||||||
_has_auto_groups = True
|
_has_auto_groups = True
|
||||||
@@ -106,14 +107,13 @@ class HasLeaderFilter(admin.SimpleListFilter):
|
|||||||
|
|
||||||
class GroupAdmin(admin.ModelAdmin):
|
class GroupAdmin(admin.ModelAdmin):
|
||||||
form = GroupAdminForm
|
form = GroupAdminForm
|
||||||
list_select_related = ('authgroup',)
|
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
list_display = (
|
list_display = (
|
||||||
'name',
|
'name',
|
||||||
'_description',
|
'_description',
|
||||||
'_properties',
|
'_properties',
|
||||||
'_member_count',
|
'_member_count',
|
||||||
'has_leader'
|
'has_leader',
|
||||||
)
|
)
|
||||||
list_filter = [
|
list_filter = [
|
||||||
'authgroup__internal',
|
'authgroup__internal',
|
||||||
@@ -129,31 +129,51 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
if _has_auto_groups:
|
has_leader_qs = (
|
||||||
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
|
AuthGroup.objects.filter(group=OuterRef('pk'), group_leaders__isnull=False)
|
||||||
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
|
|
||||||
qs = qs.annotate(
|
|
||||||
member_count=Count('user', distinct=True),
|
|
||||||
)
|
)
|
||||||
|
has_leader_groups_qs = (
|
||||||
|
AuthGroup.objects.filter(
|
||||||
|
group=OuterRef('pk'), group_leader_groups__isnull=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
qs = (
|
||||||
|
qs.select_related('authgroup')
|
||||||
|
.annotate(member_count=Count('user', distinct=True))
|
||||||
|
.annotate(has_leader=Exists(has_leader_qs))
|
||||||
|
.annotate(has_leader_groups=Exists(has_leader_groups_qs))
|
||||||
|
)
|
||||||
|
if _has_auto_groups:
|
||||||
|
is_autogroup_corp = (
|
||||||
|
Group.objects.filter(
|
||||||
|
pk=OuterRef('pk'), managedcorpgroup__isnull=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is_autogroup_alliance = (
|
||||||
|
Group.objects.filter(
|
||||||
|
pk=OuterRef('pk'), managedalliancegroup__isnull=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
qs = (
|
||||||
|
qs.annotate(is_autogroup_corp=Exists(is_autogroup_corp))
|
||||||
|
.annotate(is_autogroup_alliance=Exists(is_autogroup_alliance))
|
||||||
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _description(self, obj):
|
def _description(self, obj):
|
||||||
return obj.authgroup.description
|
return obj.authgroup.description
|
||||||
|
|
||||||
@admin.display(description="Members", ordering="member_count")
|
@admin.display(description='Members', ordering='member_count')
|
||||||
def _member_count(self, obj):
|
def _member_count(self, obj):
|
||||||
return obj.member_count
|
return obj.member_count
|
||||||
|
|
||||||
@admin.display(boolean=True)
|
@admin.display(boolean=True)
|
||||||
def has_leader(self, obj):
|
def has_leader(self, obj):
|
||||||
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
|
return obj.has_leader or obj.has_leader_groups
|
||||||
|
|
||||||
def _properties(self, obj):
|
def _properties(self, obj):
|
||||||
properties = list()
|
properties = list()
|
||||||
if _has_auto_groups and (
|
if _has_auto_groups and (obj.is_autogroup_corp or obj.is_autogroup_alliance):
|
||||||
obj.managedalliancegroup_set.exists()
|
|
||||||
or obj.managedcorpgroup_set.exists()
|
|
||||||
):
|
|
||||||
properties.append('Auto Group')
|
properties.append('Auto Group')
|
||||||
elif obj.authgroup.internal:
|
elif obj.authgroup.internal:
|
||||||
properties.append('Internal')
|
properties.append('Internal')
|
||||||
@@ -183,6 +203,8 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
ag_instance = inline_form.save(commit=False)
|
ag_instance = inline_form.save(commit=False)
|
||||||
ag_instance.group = form.instance
|
ag_instance.group = form.instance
|
||||||
ag_instance.save()
|
ag_instance.save()
|
||||||
|
if ag_instance.states.exists():
|
||||||
|
remove_users_not_matching_states_from_group.delay(ag_instance.group.pk)
|
||||||
formset.save()
|
formset.save()
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import functools
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
|
from django.contrib.auth.models import Group, User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models.functions import Lower
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -8,6 +12,39 @@ from .models import ReservedGroupName
|
|||||||
|
|
||||||
|
|
||||||
class GroupAdminForm(forms.ModelForm):
|
class GroupAdminForm(forms.ModelForm):
|
||||||
|
users = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=User.objects.order_by(Lower('username')),
|
||||||
|
required=False,
|
||||||
|
widget=FilteredSelectMultiple(verbose_name=_("Users"), is_stacked=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
self.fields["users"].initial = self.instance.user_set.all()
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
group: Group = super().save(commit=False)
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
users = self.cleaned_data["users"]
|
||||||
|
if group.pk:
|
||||||
|
self._save_m2m_and_users(group, users)
|
||||||
|
else:
|
||||||
|
self.save_m2m = functools.partial(
|
||||||
|
self._save_m2m_and_users, group=group, users=users
|
||||||
|
)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def _save_m2m_and_users(self, group, users):
|
||||||
|
"""Save m2m relations incl. users."""
|
||||||
|
group.user_set.set(users)
|
||||||
|
self._save_m2m()
|
||||||
|
|
||||||
def clean_name(self):
|
def clean_name(self):
|
||||||
my_name = self.cleaned_data['name']
|
my_name = self.cleaned_data['name']
|
||||||
if ReservedGroupName.objects.filter(name__iexact=my_name).exists():
|
if ReservedGroupName.objects.filter(name__iexact=my_name).exists():
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -14,7 +13,7 @@ from allianceauth.notifications import notify
|
|||||||
class GroupRequest(models.Model):
|
class GroupRequest(models.Model):
|
||||||
"""Request from a user for joining or leaving a group."""
|
"""Request from a user for joining or leaving a group."""
|
||||||
|
|
||||||
leave_request = models.BooleanField(default=0)
|
leave_request = models.BooleanField(default=False)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ class RequestLog(models.Model):
|
|||||||
request_type = models.BooleanField(null=True)
|
request_type = models.BooleanField(null=True)
|
||||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
request_info = models.CharField(max_length=254)
|
request_info = models.CharField(max_length=254)
|
||||||
action = models.BooleanField(default=0)
|
action = models.BooleanField(default=False)
|
||||||
request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
|
request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -189,6 +188,15 @@ class AuthGroup(models.Model):
|
|||||||
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
|
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def remove_users_not_matching_states(self):
|
||||||
|
"""Remove users not matching defined states from related group."""
|
||||||
|
states_qs = self.states.all()
|
||||||
|
if states_qs.exists():
|
||||||
|
states = list(states_qs)
|
||||||
|
non_compliant_users = self.group.user_set.exclude(profile__state__in=states)
|
||||||
|
for user in non_compliant_users:
|
||||||
|
self.group.user_set.remove(user)
|
||||||
|
|
||||||
|
|
||||||
class ReservedGroupName(models.Model):
|
class ReservedGroupName(models.Model):
|
||||||
"""Name that can not be used for groups.
|
"""Name that can not be used for groups.
|
||||||
|
|||||||
10
allianceauth/groupmanagement/tasks.py
Normal file
10
allianceauth/groupmanagement/tasks.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def remove_users_not_matching_states_from_group(group_pk: int) -> None:
|
||||||
|
"""Remove users not matching defined states from related group."""
|
||||||
|
group = Group.objects.get(pk=group_pk)
|
||||||
|
group.authgroup.remove_users_not_matching_states()
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if not auto_leave %}
|
{% if not show_leave_tab %}
|
||||||
<li>
|
<li>
|
||||||
<a data-toggle="tab" href="#leave">
|
<a data-toggle="tab" href="#leave">
|
||||||
{% translate "Leave Requests" %}
|
{% translate "Leave Requests" %}
|
||||||
@@ -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 }}
|
||||||
@@ -103,7 +102,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not auto_leave %}
|
{% if not show_leave_tab %}
|
||||||
<div id="leave" class="tab-pane">
|
<div id="leave" class="tab-pane">
|
||||||
{% if leaverequests %}
|
{% if leaverequests %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ from django.conf import settings
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import Client, RequestFactory, TestCase, override_settings
|
||||||
|
|
||||||
from allianceauth.authentication.models import CharacterOwnership, State
|
from allianceauth.authentication.models import CharacterOwnership, State
|
||||||
from allianceauth.eveonline.models import (
|
from allianceauth.eveonline.models import (
|
||||||
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
EveAllianceInfo, EveCharacter, EveCorporationInfo,
|
||||||
)
|
)
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
from . import get_admin_change_view_url
|
from ..admin import Group, GroupAdmin, HasLeaderFilter
|
||||||
from ..admin import HasLeaderFilter, GroupAdmin, Group
|
|
||||||
from ..models import ReservedGroupName
|
from ..models import ReservedGroupName
|
||||||
|
from . import get_admin_change_view_url
|
||||||
|
|
||||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||||
_has_auto_groups = True
|
_has_auto_groups = True
|
||||||
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
|
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
|
||||||
|
|
||||||
from ..admin import IsAutoGroupFilter
|
from ..admin import IsAutoGroupFilter
|
||||||
else:
|
else:
|
||||||
_has_auto_groups = False
|
_has_auto_groups = False
|
||||||
@@ -236,60 +236,104 @@ class TestGroupAdmin(TestCase):
|
|||||||
self.assertEqual(result, expected)
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
def test_member_count(self):
|
def test_member_count(self):
|
||||||
expected = 1
|
# given
|
||||||
obj = self.modeladmin.get_queryset(MockRequest(user=self.user_1))\
|
request = MockRequest(user=self.user_1)
|
||||||
.get(pk=self.group_1.pk)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
||||||
|
# when
|
||||||
result = self.modeladmin._member_count(obj)
|
result = self.modeladmin._member_count(obj)
|
||||||
self.assertEqual(result, expected)
|
# then
|
||||||
|
self.assertEqual(result, 1)
|
||||||
|
|
||||||
def test_has_leader_user(self):
|
def test_has_leader_user(self):
|
||||||
result = self.modeladmin.has_leader(self.group_1)
|
# given
|
||||||
|
request = MockRequest(user=self.user_1)
|
||||||
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin.has_leader(obj)
|
||||||
|
# then
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_has_leader_group(self):
|
def test_has_leader_group(self):
|
||||||
result = self.modeladmin.has_leader(self.group_2)
|
# given
|
||||||
|
request = MockRequest(user=self.user_1)
|
||||||
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin.has_leader(obj)
|
||||||
|
# then
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_properties_1(self):
|
def test_properties_1(self):
|
||||||
expected = ['Default']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_1)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Default'])
|
||||||
|
|
||||||
def test_properties_2(self):
|
def test_properties_2(self):
|
||||||
expected = ['Internal']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_2)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Internal'])
|
||||||
|
|
||||||
def test_properties_3(self):
|
def test_properties_3(self):
|
||||||
expected = ['Hidden']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_3)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_3.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Hidden'])
|
||||||
|
|
||||||
def test_properties_4(self):
|
def test_properties_4(self):
|
||||||
expected = ['Open']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_4)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_4.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Open'])
|
||||||
|
|
||||||
def test_properties_5(self):
|
def test_properties_5(self):
|
||||||
expected = ['Public']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_5)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_5.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Public'])
|
||||||
|
|
||||||
def test_properties_6(self):
|
def test_properties_6(self):
|
||||||
expected = ['Hidden', 'Open', 'Public']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_6)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_6.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Hidden', 'Open', 'Public'])
|
||||||
|
|
||||||
if _has_auto_groups:
|
if _has_auto_groups:
|
||||||
@patch(MODULE_PATH + '._has_auto_groups', True)
|
@patch(MODULE_PATH + '._has_auto_groups', True)
|
||||||
def test_properties_7(self):
|
def test_should_show_autogroup_for_corporation(self):
|
||||||
|
# given
|
||||||
self._create_autogroups()
|
self._create_autogroups()
|
||||||
expected = ['Auto Group']
|
request = MockRequest(user=self.user_1)
|
||||||
my_group = Group.objects\
|
queryset = self.modeladmin.get_queryset(request)
|
||||||
.filter(managedcorpgroup__isnull=False)\
|
obj = queryset.filter(managedcorpgroup__isnull=False).first()
|
||||||
.first()
|
# when
|
||||||
result = self.modeladmin._properties(my_group)
|
result = self.modeladmin._properties(obj)
|
||||||
self.assertListEqual(result, expected)
|
# then
|
||||||
|
self.assertListEqual(result, ['Auto Group'])
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '._has_auto_groups', True)
|
||||||
|
def test_should_show_autogroup_for_alliance(self):
|
||||||
|
# given
|
||||||
|
self._create_autogroups()
|
||||||
|
request = MockRequest(user=self.user_1)
|
||||||
|
queryset = self.modeladmin.get_queryset(request)
|
||||||
|
obj = queryset.filter(managedalliancegroup__isnull=False).first()
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
# then
|
||||||
|
self.assertListEqual(result, ['Auto Group'])
|
||||||
|
|
||||||
# actions
|
# actions
|
||||||
|
|
||||||
@@ -539,6 +583,142 @@ class TestGroupAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
|||||||
self.assertNotIn(field, form.fields)
|
self.assertNotIn(field, form.fields)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||||
|
class TestGroupAdmin2(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.superuser = User.objects.create_superuser("super")
|
||||||
|
|
||||||
|
def test_should_remove_users_from_state_groups(self):
|
||||||
|
# given
|
||||||
|
user_member = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character_member = AuthUtils.add_main_character_2(
|
||||||
|
user_member,
|
||||||
|
name="Bruce Wayne",
|
||||||
|
character_id=1001,
|
||||||
|
corp_id=2001,
|
||||||
|
corp_name="Wayne Technologies",
|
||||||
|
)
|
||||||
|
user_guest = AuthUtils.create_user("Lex Luthor")
|
||||||
|
AuthUtils.add_main_character_2(
|
||||||
|
user_guest,
|
||||||
|
name="Lex Luthor",
|
||||||
|
character_id=1011,
|
||||||
|
corp_id=2011,
|
||||||
|
corp_name="Luthor Corp",
|
||||||
|
)
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
member_state.member_characters.add(character_member)
|
||||||
|
user_member.refresh_from_db()
|
||||||
|
user_guest.refresh_from_db()
|
||||||
|
group = Group.objects.create(name="dummy")
|
||||||
|
user_member.groups.add(group)
|
||||||
|
user_guest.groups.add(group)
|
||||||
|
group.authgroup.states.add(member_state)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
# when
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||||
|
data={
|
||||||
|
"name": group.name,
|
||||||
|
"users": [user_member.pk, user_guest.pk],
|
||||||
|
"authgroup-TOTAL_FORMS": 1,
|
||||||
|
"authgroup-INITIAL_FORMS": 1,
|
||||||
|
"authgroup-MIN_NUM_FORMS": 0,
|
||||||
|
"authgroup-MAX_NUM_FORMS": 1,
|
||||||
|
"authgroup-0-states": member_state.pk,
|
||||||
|
"authgroup-0-internal": "on",
|
||||||
|
"authgroup-0-hidden": "on",
|
||||||
|
"authgroup-0-group": group.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||||
|
self.assertIn(group, user_member.groups.all())
|
||||||
|
self.assertNotIn(group, user_guest.groups.all())
|
||||||
|
|
||||||
|
def test_should_add_user_to_existing_group(self):
|
||||||
|
# given
|
||||||
|
user_bruce = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
user_lex = AuthUtils.create_user("Lex Luthor")
|
||||||
|
group = Group.objects.create(name="dummy")
|
||||||
|
user_bruce.groups.add(group)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
# when
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||||
|
data={
|
||||||
|
"name": group.name,
|
||||||
|
"users": [user_bruce.pk, user_lex.pk],
|
||||||
|
"authgroup-TOTAL_FORMS": 1,
|
||||||
|
"authgroup-INITIAL_FORMS": 1,
|
||||||
|
"authgroup-MIN_NUM_FORMS": 0,
|
||||||
|
"authgroup-MAX_NUM_FORMS": 1,
|
||||||
|
"authgroup-0-internal": "on",
|
||||||
|
"authgroup-0-hidden": "on",
|
||||||
|
"authgroup-0-group": group.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||||
|
self.assertIn(group, user_bruce.groups.all())
|
||||||
|
self.assertIn(group, user_lex.groups.all())
|
||||||
|
|
||||||
|
def test_should_remove_user_from_existing_group(self):
|
||||||
|
# given
|
||||||
|
user_bruce = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
user_lex = AuthUtils.create_user("Lex Luthor")
|
||||||
|
group = Group.objects.create(name="dummy")
|
||||||
|
user_bruce.groups.add(group)
|
||||||
|
user_lex.groups.add(group)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
# when
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||||
|
data={
|
||||||
|
"name": group.name,
|
||||||
|
"users": user_bruce.pk,
|
||||||
|
"authgroup-TOTAL_FORMS": 1,
|
||||||
|
"authgroup-INITIAL_FORMS": 1,
|
||||||
|
"authgroup-MIN_NUM_FORMS": 0,
|
||||||
|
"authgroup-MAX_NUM_FORMS": 1,
|
||||||
|
"authgroup-0-internal": "on",
|
||||||
|
"authgroup-0-hidden": "on",
|
||||||
|
"authgroup-0-group": group.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||||
|
self.assertIn(group, user_bruce.groups.all())
|
||||||
|
self.assertNotIn(group, user_lex.groups.all())
|
||||||
|
|
||||||
|
def test_should_include_user_when_creating_group(self):
|
||||||
|
# given
|
||||||
|
user_bruce = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
# when
|
||||||
|
response = self.client.post(
|
||||||
|
"/admin/groupmanagement/group/add/",
|
||||||
|
data={
|
||||||
|
"name": "new group",
|
||||||
|
"users": user_bruce.pk,
|
||||||
|
"authgroup-TOTAL_FORMS": 1,
|
||||||
|
"authgroup-INITIAL_FORMS": 0,
|
||||||
|
"authgroup-MIN_NUM_FORMS": 0,
|
||||||
|
"authgroup-MAX_NUM_FORMS": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||||
|
group = Group.objects.get(name="new group")
|
||||||
|
self.assertIn(group, user_bruce.groups.all())
|
||||||
|
|
||||||
|
|
||||||
class TestReservedGroupNameAdmin(TestCase):
|
class TestReservedGroupNameAdmin(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|||||||
@@ -232,6 +232,38 @@ class TestAuthGroup(TestCase):
|
|||||||
expected = 'Superheros'
|
expected = 'Superheros'
|
||||||
self.assertEqual(str(group.authgroup), expected)
|
self.assertEqual(str(group.authgroup), expected)
|
||||||
|
|
||||||
|
def test_should_remove_guests_from_group_when_restricted_to_members_only(self):
|
||||||
|
# given
|
||||||
|
user_member = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character_member = AuthUtils.add_main_character_2(
|
||||||
|
user_member,
|
||||||
|
name="Bruce Wayne",
|
||||||
|
character_id=1001,
|
||||||
|
corp_id=2001,
|
||||||
|
corp_name="Wayne Technologies",
|
||||||
|
)
|
||||||
|
user_guest = AuthUtils.create_user("Lex Luthor")
|
||||||
|
AuthUtils.add_main_character_2(
|
||||||
|
user_guest,
|
||||||
|
name="Lex Luthor",
|
||||||
|
character_id=1011,
|
||||||
|
corp_id=2011,
|
||||||
|
corp_name="Luthor Corp",
|
||||||
|
)
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
member_state.member_characters.add(character_member)
|
||||||
|
user_member.refresh_from_db()
|
||||||
|
user_guest.refresh_from_db()
|
||||||
|
group = Group.objects.create(name="dummy")
|
||||||
|
user_member.groups.add(group)
|
||||||
|
user_guest.groups.add(group)
|
||||||
|
group.authgroup.states.add(member_state)
|
||||||
|
# when
|
||||||
|
group.authgroup.remove_users_not_matching_states()
|
||||||
|
# then
|
||||||
|
self.assertIn(group, user_member.groups.all())
|
||||||
|
self.assertNotIn(group, user_guest.groups.all())
|
||||||
|
|
||||||
|
|
||||||
class TestAuthGroupRequestApprovers(TestCase):
|
class TestAuthGroupRequestApprovers(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.test import RequestFactory, TestCase, override_settings
|
from django.test import RequestFactory, TestCase, override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from allianceauth.groupmanagement.models import Group, GroupRequest
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
from .. import views
|
from .. import views
|
||||||
@@ -16,6 +17,7 @@ class TestViews(TestCase):
|
|||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = AuthUtils.create_user('Peter Parker')
|
self.user = AuthUtils.create_user('Peter Parker')
|
||||||
self.user_with_manage_permission = AuthUtils.create_user('Bruce Wayne')
|
self.user_with_manage_permission = AuthUtils.create_user('Bruce Wayne')
|
||||||
|
self.group = Group.objects.create(name="Example group")
|
||||||
|
|
||||||
# set permissions
|
# set permissions
|
||||||
AuthUtils.add_permission_to_user_by_name(
|
AuthUtils.add_permission_to_user_by_name(
|
||||||
@@ -83,3 +85,19 @@ class TestViews(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertNotIn('<a data-toggle="tab" href="#leave">', content)
|
self.assertNotIn('<a data-toggle="tab" href="#leave">', content)
|
||||||
self.assertNotIn('<div id="leave" class="tab-pane">', content)
|
self.assertNotIn('<div id="leave" class="tab-pane">', content)
|
||||||
|
|
||||||
|
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
|
||||||
|
def test_should_not_hide_leave_requests_tab_when_there_are_open_requests(self):
|
||||||
|
# given
|
||||||
|
request = self.factory.get(reverse('groupmanagement:management'))
|
||||||
|
request.user = self.user_with_manage_permission
|
||||||
|
GroupRequest.objects.create(user=self.user, group=self.group, leave_request=True)
|
||||||
|
|
||||||
|
# when
|
||||||
|
response = views.group_management(request)
|
||||||
|
|
||||||
|
# then
|
||||||
|
content = response_content_to_str(response)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('<a data-toggle="tab" href="#leave">', content)
|
||||||
|
self.assertIn('<div id="leave" class="tab-pane">', content)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
13
allianceauth/groupmanagement/views.py
Executable file → Normal file
13
allianceauth/groupmanagement/views.py
Executable file → Normal file
@@ -2,13 +2,12 @@ import logging
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth.notifications import notify
|
from allianceauth.notifications import notify
|
||||||
@@ -16,7 +15,6 @@ from allianceauth.notifications import notify
|
|||||||
from .managers import GroupManager
|
from .managers import GroupManager
|
||||||
from .models import GroupRequest, RequestLog
|
from .models import GroupRequest, RequestLog
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -45,10 +43,15 @@ def group_management(request):
|
|||||||
logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format(
|
logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format(
|
||||||
request.user, len(acceptrequests), len(leaverequests)))
|
request.user, len(acceptrequests), len(leaverequests)))
|
||||||
|
|
||||||
|
show_leave_tab = (
|
||||||
|
getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False)
|
||||||
|
and not GroupRequest.objects.filter(leave_request=True).exists()
|
||||||
|
)
|
||||||
|
|
||||||
render_items = {
|
render_items = {
|
||||||
'acceptrequests': acceptrequests,
|
'acceptrequests': acceptrequests,
|
||||||
'leaverequests': leaverequests,
|
'leaverequests': leaverequests,
|
||||||
'auto_leave': getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False),
|
'show_leave_tab': show_leave_tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'groupmanagement/index.html', context=render_items)
|
return render(request, 'groupmanagement/index.html', context=render_items)
|
||||||
|
|||||||
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user