mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 14:16:21 +01:00
Compare commits
407 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e416ab8ff2 | ||
|
|
2802ed03a5 | ||
|
|
4af73c76fe | ||
|
|
b6149979aa | ||
|
|
cb20288427 | ||
|
|
db6f4c91dc | ||
|
|
57ac7a5277 | ||
|
|
136438f9c2 | ||
|
|
e2be8b3440 | ||
|
|
04f3473ef3 | ||
|
|
255cb0da8d | ||
|
|
069352fb0f | ||
|
|
66e8ddb684 | ||
|
|
179c26975c | ||
|
|
e17f6e799b | ||
|
|
7cd8294104 | ||
|
|
ede5540335 | ||
|
|
747279b773 | ||
|
|
44f8b1c477 | ||
|
|
7c6ebd9bf6 | ||
|
|
430469b708 | ||
|
|
efbb3cee31 | ||
|
|
21094ed4dd | ||
|
|
5f326efc7e | ||
|
|
b6e34ace35 | ||
|
|
fe4a8965e3 | ||
|
|
23371c233d | ||
|
|
7a3bbf0d7f | ||
|
|
89a1bec9c1 | ||
|
|
1c1e70619a | ||
|
|
0ff4374efa | ||
|
|
18d0e58a48 | ||
|
|
84f44338dc | ||
|
|
2ba0412890 | ||
|
|
2326522b29 | ||
|
|
a7cb6ee434 | ||
|
|
2aeef63565 | ||
|
|
3c9e7335ef | ||
|
|
49067de325 | ||
|
|
471e7e29ae | ||
|
|
3ec5775406 | ||
|
|
e804d2b60d | ||
|
|
742438a95d | ||
|
|
5c60086baa | ||
|
|
e49041bb14 | ||
|
|
f3cbe91883 | ||
|
|
ea439a2176 | ||
|
|
56e1e76f11 | ||
|
|
634e7357be | ||
|
|
08dc88da1a | ||
|
|
3d206e445c | ||
|
|
64686cdad1 | ||
|
|
d7fe09bdf1 | ||
|
|
6da50da92f | ||
|
|
51e4dd986f | ||
|
|
bee6522182 | ||
|
|
1711a9dd33 | ||
|
|
3914626379 | ||
|
|
df276cb32d | ||
|
|
daad7d8b10 | ||
|
|
3bf5bc0fe3 | ||
|
|
96abae553a | ||
|
|
f9cbfb1562 | ||
|
|
8eaa94e179 | ||
|
|
4f876b648b | ||
|
|
cd738137c0 | ||
|
|
5605eb129d | ||
|
|
87ef0f21a3 | ||
|
|
a1c7ce827e | ||
|
|
97466bcdfb | ||
|
|
ff3096b106 | ||
|
|
98f0d77f3f | ||
|
|
92548ba402 | ||
|
|
c46741d311 | ||
|
|
7c7c1abf7c | ||
|
|
fc303b1b0a | ||
|
|
4e220a9679 | ||
|
|
b17b1f7504 | ||
|
|
7081fc0e76 | ||
|
|
68e4574f19 | ||
|
|
e6e0a70012 | ||
|
|
13e38da942 | ||
|
|
468c1de26b | ||
|
|
22ef5ac0e5 | ||
|
|
ef2dc08958 | ||
|
|
6b84ffa16c | ||
|
|
d7a1096413 | ||
|
|
93b94a8bc2 | ||
|
|
9a95716105 | ||
|
|
dbfcf5d87a | ||
|
|
105d7d53b3 | ||
|
|
01cefe1457 | ||
|
|
7fe3db8017 | ||
|
|
e0945fac80 | ||
|
|
40fa190820 | ||
|
|
670580f8f3 | ||
|
|
323a0bcf16 | ||
|
|
6e995edd80 | ||
|
|
8d86e45b7a | ||
|
|
2aa6df4461 | ||
|
|
cf6f989502 | ||
|
|
3e1d8ae334 | ||
|
|
bcfe9484b5 | ||
|
|
5e4d1b9cfd | ||
|
|
3b463e7305 | ||
|
|
eedf5082fa | ||
|
|
2ea5b15175 | ||
|
|
7a9808aad3 | ||
|
|
a1d712694c | ||
|
|
ca11c726a3 | ||
|
|
6e0f7a35bd | ||
|
|
7375b001ca | ||
|
|
0287086633 | ||
|
|
9eb2becbb5 | ||
|
|
12f1444fe7 | ||
|
|
d6372bd093 | ||
|
|
3935a9cdd2 | ||
|
|
49fb6c39d5 | ||
|
|
8821f18b21 | ||
|
|
f4d8ead54e | ||
|
|
7427e13505 | ||
|
|
445683c3d5 | ||
|
|
677c46c48a | ||
|
|
87b9e3f87a | ||
|
|
da2a5aff2f | ||
|
|
65d77743dc | ||
|
|
1c7f8256d0 | ||
|
|
61f0aae5d9 | ||
|
|
4d56030edf | ||
|
|
2dfe194a3b | ||
|
|
ebb40deb7f | ||
|
|
a3a2d3d35b | ||
|
|
d4dee519b8 | ||
|
|
dfcbad3476 | ||
|
|
e03c1307a3 | ||
|
|
3a95b89779 | ||
|
|
4f5b231bdf | ||
|
|
40c0b8d862 | ||
|
|
62c936f1c0 | ||
|
|
2a762df9b3 | ||
|
|
8fb5a488f7 | ||
|
|
dc239d5396 | ||
|
|
2815ebaa07 | ||
|
|
34dd4802a8 | ||
|
|
3ebf11308c | ||
|
|
5cd5180a91 | ||
|
|
ba213db493 | ||
|
|
8362d11714 | ||
|
|
7ba65968ed | ||
|
|
7fdbac20cb | ||
|
|
76efcb5266 | ||
|
|
6aacb8c2e3 | ||
|
|
465fba3a18 | ||
|
|
9ae6addc71 | ||
|
|
76ae9b8849 | ||
|
|
13a8b7678f | ||
|
|
c166fc0ef9 | ||
|
|
9da61588eb | ||
|
|
7425176b3f | ||
|
|
2bb518f7e0 | ||
|
|
84d5693583 | ||
|
|
867e2a1ded | ||
|
|
fd6c6991f5 | ||
|
|
333fe8497d | ||
|
|
8a3fb17147 | ||
|
|
f8baeb19a7 | ||
|
|
10096862e5 | ||
|
|
89193d2fcf | ||
|
|
fd08b987bd | ||
|
|
4e9e22cb4b | ||
|
|
f8fbbb5ba7 | ||
|
|
dd3ef41396 | ||
|
|
d5fda05dc9 | ||
|
|
d815fad0e6 | ||
|
|
e109198782 | ||
|
|
fbd4672454 | ||
|
|
a29bd567c2 | ||
|
|
960aef95ad | ||
|
|
4aae5497bb | ||
|
|
6081cbe900 | ||
|
|
ce0d8342e3 | ||
|
|
006785e592 | ||
|
|
df05070a55 | ||
|
|
e81450baf3 | ||
|
|
24b6c19aca | ||
|
|
9f4bf13cc9 | ||
|
|
2a156302f0 | ||
|
|
c4d3bde106 | ||
|
|
0900806f68 | ||
|
|
a0d32d8c2d | ||
|
|
e16252842a | ||
|
|
950ae34093 | ||
|
|
0ba94c53aa | ||
|
|
96cc55d174 | ||
|
|
a9062c4389 | ||
|
|
51b86f88b9 | ||
|
|
8184461b48 | ||
|
|
ca0cdd6e15 | ||
|
|
036a17ad3b | ||
|
|
2418023ddd | ||
|
|
c602cf0b00 | ||
|
|
b28b51916c | ||
|
|
0db0978d5f | ||
|
|
0531da1128 | ||
|
|
547d047f59 | ||
|
|
5cd92e29d3 | ||
|
|
bb44194cfc | ||
|
|
aa1cb96c8a | ||
|
|
40ae092306 | ||
|
|
b2e70c3702 | ||
|
|
eebeb26001 | ||
|
|
003a67224e | ||
|
|
d3a3810456 | ||
|
|
f55008559e | ||
|
|
dbc19c76c5 | ||
|
|
ced7972962 | ||
|
|
ad508bd880 | ||
|
|
5c128f2c78 | ||
|
|
1caaca86cf | ||
|
|
33ad1413d5 | ||
|
|
cbb5c80b94 | ||
|
|
d2edd288f9 | ||
|
|
21e80f6961 | ||
|
|
a747951d19 | ||
|
|
4cc7135ace | ||
|
|
6a990c11e6 | ||
|
|
3f47cecbfc | ||
|
|
77c126ea2f | ||
|
|
8b7e57494c | ||
|
|
8ff3d854ba | ||
|
|
49ff355d50 | ||
|
|
6f670da1db | ||
|
|
c558f3785f | ||
|
|
04cdd4c64f | ||
|
|
00fcebd8e3 | ||
|
|
995c84481c | ||
|
|
4dd42da993 | ||
|
|
8295cc51a3 | ||
|
|
f7e1d7c47e | ||
|
|
234191218a | ||
|
|
de12b49527 | ||
|
|
021b7b2edb | ||
|
|
20b959f273 | ||
|
|
e16bb2a737 | ||
|
|
0a6a6e0bf9 | ||
|
|
f0fe3929d4 | ||
|
|
9a62c729eb | ||
|
|
eaedcd5bb7 | ||
|
|
d742257c74 | ||
|
|
ad92d7a916 | ||
|
|
af22222bdf | ||
|
|
01f49bd125 | ||
|
|
af5930b9d0 | ||
|
|
95ba19a827 | ||
|
|
c75647301f | ||
|
|
adedf7534f | ||
|
|
d0684862fe | ||
|
|
2440e5d2b2 | ||
|
|
5f51ad4a6a | ||
|
|
0bd3acc230 | ||
|
|
11a32c90a8 | ||
|
|
6f0b853a60 | ||
|
|
583a6d4c7f | ||
|
|
155494afea | ||
|
|
b0aa58b910 | ||
|
|
1adce85422 | ||
|
|
cd47eadcdc | ||
|
|
247058a30f | ||
|
|
e0fa615e90 | ||
|
|
61ec67183c | ||
|
|
4369813478 | ||
|
|
8bb3d35252 | ||
|
|
398a980fb5 | ||
|
|
e14a295ce6 | ||
|
|
e0cd5c6fb9 | ||
|
|
c37ece49d5 | ||
|
|
8adab8bae0 | ||
|
|
a9c87bc25a | ||
|
|
a4901802c0 | ||
|
|
079dcc5d28 | ||
|
|
10c5a8906d | ||
|
|
e6306ea7aa | ||
|
|
20067c1133 | ||
|
|
dba3c651dc | ||
|
|
0b4d2b819b | ||
|
|
7eebf4d953 | ||
|
|
9e45d2eac7 | ||
|
|
e3017f1ec7 | ||
|
|
12709b1b56 | ||
|
|
51ae604efd | ||
|
|
63071ec359 | ||
|
|
0032e4a01f | ||
|
|
25ab78a41e | ||
|
|
3aa0e323d2 | ||
|
|
cadbb7e61c | ||
|
|
115263eb5a | ||
|
|
18fec5f614 | ||
|
|
02ab064ec3 | ||
|
|
ba25f99cb4 | ||
|
|
28fd1b07ea | ||
|
|
5de19c43df | ||
|
|
6a0ddc9a83 | ||
|
|
03be66d11f | ||
|
|
3d92008069 | ||
|
|
59e47c24c2 | ||
|
|
6d942555ff | ||
|
|
7e312bb95f | ||
|
|
c92fee78e2 | ||
|
|
658a8cd6ce | ||
|
|
c1dc130766 | ||
|
|
35f5573b63 | ||
|
|
a9ebecdec6 | ||
|
|
21f0a96422 | ||
|
|
9e47d19337 | ||
|
|
2c5972d0ab | ||
|
|
ee41d62c13 | ||
|
|
346b4014a9 | ||
|
|
9b56a441ed | ||
|
|
068bf1ae7a | ||
|
|
5be686e3ca | ||
|
|
a215b4411c | ||
|
|
e15cfa0fb1 | ||
|
|
46d51699f4 | ||
|
|
ff30a136d5 | ||
|
|
6dcf3304d5 | ||
|
|
beddeea338 | ||
|
|
69723937f7 | ||
|
|
c541f56ee2 | ||
|
|
7e887e5e34 | ||
|
|
072327c79f | ||
|
|
28af3ff11e | ||
|
|
e3b151f2fb | ||
|
|
f87d7dbdf8 | ||
|
|
a04e6ae3d0 | ||
|
|
15042f5e77 | ||
|
|
6e25361d5e | ||
|
|
9e639a0eeb | ||
|
|
257fbdef36 | ||
|
|
df003c8ec5 | ||
|
|
ba22685eb8 | ||
|
|
773288072a | ||
|
|
63afb13d25 | ||
|
|
5dd286bbe7 | ||
|
|
8aaa8172ca | ||
|
|
b68b401146 | ||
|
|
f7821b647f | ||
|
|
a6526d6f78 | ||
|
|
7898594909 | ||
|
|
cfd12ee3cc | ||
|
|
2c9177b19f | ||
|
|
abff26fb6e | ||
|
|
e8c3b5225c | ||
|
|
98fd1dcc4c | ||
|
|
cfe46e4ca5 | ||
|
|
4675193416 | ||
|
|
a84fa1ca69 | ||
|
|
8f6cb0b9bb | ||
|
|
1c8634f1c8 | ||
|
|
2a21599d45 | ||
|
|
e379c01655 | ||
|
|
afa3d2e7cc | ||
|
|
e5ed33aeec | ||
|
|
b12471e775 | ||
|
|
5e70dab11f | ||
|
|
f728c786b3 | ||
|
|
7056912d54 | ||
|
|
7efed950ca | ||
|
|
886acf2005 | ||
|
|
b2dec3bff2 | ||
|
|
f0a402e141 | ||
|
|
2e2afd7923 | ||
|
|
e9ea09bc56 | ||
|
|
186fa1be03 | ||
|
|
37d1d84fc3 | ||
|
|
ee24706e43 | ||
|
|
07e85727ea | ||
|
|
4912f0f8f0 | ||
|
|
24376262f0 | ||
|
|
efe0c6963b | ||
|
|
a4644028ae | ||
|
|
3a77b4a429 | ||
|
|
fa375a551c | ||
|
|
00a93e6fe9 | ||
|
|
656e69d4b2 | ||
|
|
3b55d370d0 | ||
|
|
5c126ffe82 | ||
|
|
99be753836 | ||
|
|
2e78aa5f26 | ||
|
|
567d97f38a | ||
|
|
d6821b3fd6 | ||
|
|
90375246fd | ||
|
|
a2f217ace5 | ||
|
|
25cf2fdcd5 | ||
|
|
4305ae7995 | ||
|
|
4aff4006e3 | ||
|
|
55c188f2d0 | ||
|
|
f36f824a4b | ||
|
|
6fbf33bcdd | ||
|
|
ed3c2c8529 | ||
|
|
05d7fb1f63 | ||
|
|
3b19db2564 | ||
|
|
98aa44c070 | ||
|
|
8d46ee65af | ||
|
|
49780b871d | ||
|
|
2b7d24fc28 | ||
|
|
b8f86a618f | ||
|
|
9921011742 |
@@ -19,5 +19,6 @@ exclude_lines =
|
||||
if __name__ == .__main__.:
|
||||
def __repr__
|
||||
raise AssertionError
|
||||
if TYPE_CHECKING:
|
||||
|
||||
ignore_errors = True
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -73,3 +73,4 @@ celerybeat-schedule
|
||||
.flake8
|
||||
.pylintrc
|
||||
Makefile
|
||||
alliance_auth.sqlite3
|
||||
|
||||
@@ -25,12 +25,12 @@ before_script:
|
||||
pre-commit-check:
|
||||
<<: *only-default
|
||||
stage: pre-commit
|
||||
image: python:3.10-bullseye
|
||||
variables:
|
||||
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||
cache:
|
||||
paths:
|
||||
- ${PRE_COMMIT_HOME}
|
||||
image: python:3.11-bookworm
|
||||
# variables:
|
||||
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||
# cache:
|
||||
# paths:
|
||||
# - ${PRE_COMMIT_HOME}
|
||||
script:
|
||||
- pip install pre-commit
|
||||
- pre-commit run --all-files
|
||||
@@ -53,7 +53,7 @@ secret_detection:
|
||||
|
||||
test-3.8-core:
|
||||
<<: *only-default
|
||||
image: python:3.8-bullseye
|
||||
image: python:3.8-bookworm
|
||||
script:
|
||||
- tox -e py38-core
|
||||
artifacts:
|
||||
@@ -65,7 +65,7 @@ test-3.8-core:
|
||||
|
||||
test-3.9-core:
|
||||
<<: *only-default
|
||||
image: python:3.9-bullseye
|
||||
image: python:3.9-bookworm
|
||||
script:
|
||||
- tox -e py39-core
|
||||
artifacts:
|
||||
@@ -77,7 +77,7 @@ test-3.9-core:
|
||||
|
||||
test-3.10-core:
|
||||
<<: *only-default
|
||||
image: python:3.10-bullseye
|
||||
image: python:3.10-bookworm
|
||||
script:
|
||||
- tox -e py310-core
|
||||
artifacts:
|
||||
@@ -89,7 +89,7 @@ test-3.10-core:
|
||||
|
||||
test-3.11-core:
|
||||
<<: *only-default
|
||||
image: python:3.11-bullseye
|
||||
image: python:3.11-bookworm
|
||||
script:
|
||||
- tox -e py311-core
|
||||
artifacts:
|
||||
@@ -99,22 +99,21 @@ test-3.11-core:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
test-pvpy-core:
|
||||
test-3.12-core:
|
||||
<<: *only-default
|
||||
image: pypy:3.9-bullseye
|
||||
image: python:3.12-bookworm
|
||||
script:
|
||||
- tox -e pypy-all
|
||||
- tox -e py312-core
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
test-3.8-all:
|
||||
<<: *only-default
|
||||
image: python:3.8-bullseye
|
||||
image: python:3.8-bookworm
|
||||
script:
|
||||
- tox -e py38-all
|
||||
artifacts:
|
||||
@@ -126,7 +125,7 @@ test-3.8-all:
|
||||
|
||||
test-3.9-all:
|
||||
<<: *only-default
|
||||
image: python:3.9-bullseye
|
||||
image: python:3.9-bookworm
|
||||
script:
|
||||
- tox -e py39-all
|
||||
artifacts:
|
||||
@@ -138,7 +137,7 @@ test-3.9-all:
|
||||
|
||||
test-3.10-all:
|
||||
<<: *only-default
|
||||
image: python:3.10-bullseye
|
||||
image: python:3.10-bookworm
|
||||
script:
|
||||
- tox -e py310-all
|
||||
artifacts:
|
||||
@@ -150,7 +149,7 @@ test-3.10-all:
|
||||
|
||||
test-3.11-all:
|
||||
<<: *only-default
|
||||
image: python:3.11-bullseye
|
||||
image: python:3.11-bookworm
|
||||
script:
|
||||
- tox -e py311-all
|
||||
artifacts:
|
||||
@@ -161,22 +160,21 @@ test-3.11-all:
|
||||
path: coverage.xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
|
||||
test-pvpy-all:
|
||||
test-3.12-all:
|
||||
<<: *only-default
|
||||
image: pypy:3.9-bullseye
|
||||
image: python:3.12-bookworm
|
||||
script:
|
||||
- tox -e pypy-all
|
||||
- tox -e py312-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
allow_failure: true
|
||||
|
||||
build-test:
|
||||
stage: test
|
||||
image: python:3.10-bullseye
|
||||
image: python:3.11-bookworm
|
||||
|
||||
before_script:
|
||||
- python -m pip install --upgrade pip
|
||||
@@ -195,13 +193,13 @@ build-test:
|
||||
|
||||
test-docs:
|
||||
<<: *only-default
|
||||
image: python:3.11-bullseye
|
||||
image: python:3.11-bookworm
|
||||
script:
|
||||
- tox -e docs
|
||||
- tox -e docs
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
image: python:3.10-bullseye
|
||||
image: python:3.11-bookworm
|
||||
|
||||
before_script:
|
||||
- python -m pip install --upgrade pip
|
||||
@@ -217,10 +215,10 @@ deploy_production:
|
||||
|
||||
build-image:
|
||||
before_script: []
|
||||
image: docker:20.10.10
|
||||
image: docker:24.0
|
||||
stage: docker
|
||||
services:
|
||||
- docker:20.10.10-dind
|
||||
- docker:24.0-dind
|
||||
script: |
|
||||
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
|
||||
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
|
||||
@@ -230,12 +228,10 @@ build-image:
|
||||
LATEST_TAG=$CI_REGISTRY_IMAGE/auth:latest
|
||||
|
||||
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
|
||||
docker tag $IMAGE_TAG $CURRENT_TAG
|
||||
docker tag $IMAGE_TAG $MINOR_TAG
|
||||
docker tag $IMAGE_TAG $MAJOR_TAG
|
||||
docker tag $IMAGE_TAG $LATEST_TAG
|
||||
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
|
||||
docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
docker buildx create --use --name new-builder
|
||||
docker buildx build . --tag $IMAGE_TAG --tag $CURRENT_TAG --tag $MINOR_TAG --tag $MAJOR_TAG --tag $LATEST_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: delayed
|
||||
@@ -243,17 +239,19 @@ build-image:
|
||||
|
||||
build-image-dev:
|
||||
before_script: []
|
||||
image: docker:20.10.10
|
||||
image: docker:24.0
|
||||
stage: docker
|
||||
services:
|
||||
- docker:20.10.10-dind
|
||||
- docker:24.0-dind
|
||||
script: |
|
||||
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
|
||||
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
|
||||
|
||||
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
|
||||
docker push $IMAGE_TAG
|
||||
docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
docker buildx create --use --name new-builder
|
||||
docker buildx build . --tag $IMAGE_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
|
||||
when: manual
|
||||
@@ -262,17 +260,19 @@ build-image-dev:
|
||||
|
||||
build-image-mr:
|
||||
before_script: []
|
||||
image: docker:20.10.10
|
||||
image: docker:24.0
|
||||
stage: docker
|
||||
services:
|
||||
- docker:20.10.10-dind
|
||||
- docker:24.0-dind
|
||||
script: |
|
||||
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
|
||||
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA
|
||||
|
||||
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
|
||||
docker push $IMAGE_TAG
|
||||
docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
docker buildx create --use --name new-builder
|
||||
docker buildx build . --tag $IMAGE_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: manual
|
||||
|
||||
@@ -4,8 +4,21 @@
|
||||
# pre-commit autoupdate
|
||||
|
||||
repos:
|
||||
# Code Upgrades
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.17.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [--target-version=4.2]
|
||||
|
||||
# Formatting
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
# Identify invalid files
|
||||
- id: check-ast
|
||||
@@ -13,27 +26,24 @@ repos:
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-xml
|
||||
|
||||
# git checks
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
args: [ --maxkb=1000 ]
|
||||
args: [--maxkb=1000]
|
||||
- id: detect-private-key
|
||||
- id: check-case-conflict
|
||||
|
||||
# Python checks
|
||||
# - id: check-docstring-first
|
||||
# - id: check-docstring-first
|
||||
- id: debug-statements
|
||||
# - id: requirements-txt-fixer
|
||||
# - id: requirements-txt-fixer
|
||||
- id: fix-encoding-pragma
|
||||
args: [ --remove ]
|
||||
args: [--remove]
|
||||
- id: fix-byte-order-marker
|
||||
|
||||
# General quality checks
|
||||
- id: mixed-line-ending
|
||||
args: [ --fix=lf ]
|
||||
args: [--fix=lf]
|
||||
- id: trailing-whitespace
|
||||
args: [ --markdown-linebreak-ext=md ]
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
exclude: |
|
||||
(?x)(
|
||||
\.min\.css|
|
||||
@@ -52,9 +62,8 @@ repos:
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 2.7.2
|
||||
rev: 2.7.3
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
exclude: |
|
||||
@@ -65,15 +74,26 @@ repos:
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.14.0
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.41.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [ --target-version=4.0 ]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.10.1
|
||||
- id: markdownlint
|
||||
args:
|
||||
- --disable=MD013
|
||||
# Infrastructure
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 2.1.3
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [ --py38-plus ]
|
||||
- id: pyproject-fmt
|
||||
name: pyproject.toml formatter
|
||||
description: "Format the pyproject.toml file."
|
||||
args:
|
||||
- --indent=4
|
||||
additional_dependencies:
|
||||
- tox==4.15.0 # https://github.com/tox-dev/tox/releases/latest
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.18
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
name: Validate pyproject.toml
|
||||
description: "Validate the pyproject.toml file."
|
||||
|
||||
@@ -12,6 +12,9 @@ build:
|
||||
- redis
|
||||
tools:
|
||||
python: "3.11"
|
||||
jobs:
|
||||
post_system_dependencies:
|
||||
- redis-server --daemonize yes
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
|
||||
10
.tx/config
10
.tx/config
@@ -1,10 +0,0 @@
|
||||
[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
|
||||
@@ -5,7 +5,7 @@ manage online service access.
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
__version__ = '3.8.1'
|
||||
__version__ = '4.2.0'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = f'{__title__} v{__version__}'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import AnalyticsIdentifier, AnalyticsPath, AnalyticsTokens
|
||||
from .models import AnalyticsIdentifier, AnalyticsTokens
|
||||
|
||||
|
||||
@admin.register(AnalyticsIdentifier)
|
||||
@@ -13,9 +13,3 @@ class AnalyticsIdentifierAdmin(admin.ModelAdmin):
|
||||
class AnalyticsTokensAdmin(admin.ModelAdmin):
|
||||
search_fields = ['name', ]
|
||||
list_display = ('name', 'type',)
|
||||
|
||||
|
||||
@admin.register(AnalyticsPath)
|
||||
class AnalyticsPathAdmin(admin.ModelAdmin):
|
||||
search_fields = ['ignore_path', ]
|
||||
list_display = ('ignore_path',)
|
||||
|
||||
@@ -4,6 +4,3 @@ from django.apps import AppConfig
|
||||
class AnalyticsConfig(AppConfig):
|
||||
name = 'allianceauth.analytics'
|
||||
label = 'analytics'
|
||||
|
||||
def ready(self):
|
||||
import allianceauth.analytics.signals
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
"model": "analytics.AnalyticsTokens",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "AA Team Public Google Analytics (Universal)",
|
||||
"name": "AA Team Public Google Analytics (V4)",
|
||||
"type": "GA-V4",
|
||||
"token": "UA-186249766-2",
|
||||
"send_page_views": "False",
|
||||
"send_celery_tasks": "False",
|
||||
"token": "G-6LYSMYK8DE",
|
||||
"secret": "KLlpjLZ-SRGozS5f5wb_kw",
|
||||
"send_stats": "False"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||
from .tasks import send_ga_tracking_web_view
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class AnalyticsMiddleware(MiddlewareMixin):
|
||||
def process_response(self, request, response):
|
||||
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
|
||||
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||
return response
|
||||
analyticstokens = AnalyticsTokens.objects.all()
|
||||
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
|
||||
try:
|
||||
title = BeautifulSoup(
|
||||
response.content, "html.parser").html.head.title.text
|
||||
except AttributeError:
|
||||
title = ''
|
||||
for token in analyticstokens:
|
||||
# Check if Page View Sending is Disabled
|
||||
if token.send_page_views is False:
|
||||
continue
|
||||
# Check Exclusions
|
||||
ignore = False
|
||||
for ignore_path in token.ignore_paths.values():
|
||||
ignore_path_regex = re.compile(ignore_path["ignore_path"])
|
||||
if re.search(ignore_path_regex, request.path) is not None:
|
||||
ignore = True
|
||||
|
||||
if ignore is True:
|
||||
continue
|
||||
|
||||
tracking_id = token.token
|
||||
locale = request.LANGUAGE_CODE
|
||||
path = request.path
|
||||
try:
|
||||
useragent = request.headers["User-Agent"]
|
||||
except KeyError:
|
||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
|
||||
send_ga_tracking_web_view.s(tracking_id=tracking_id,
|
||||
client_id=client_id,
|
||||
page=path,
|
||||
title=title,
|
||||
locale=locale,
|
||||
useragent=useragent).\
|
||||
apply_async(priority=9)
|
||||
return response
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.6 on 2022-08-30 05:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('analytics', '0006_more_ignore_paths'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='analyticstokens',
|
||||
name='secret',
|
||||
field=models.CharField(blank=True, max_length=254),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-30 08:53
|
||||
|
||||
from django.db import migrations
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
def add_aa_team_token(apps, schema_editor):
|
||||
# We can't import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||
token = Tokens()
|
||||
try:
|
||||
ua_token = Tokens.objects.get(token="UA-186249766-2")
|
||||
original_send_page_views = ua_token.send_page_views
|
||||
original_send_celery_tasks = ua_token.send_celery_tasks
|
||||
original_send_stats = ua_token.send_stats
|
||||
except ObjectDoesNotExist:
|
||||
original_send_page_views = True
|
||||
original_send_celery_tasks = True
|
||||
original_send_stats = True
|
||||
|
||||
try:
|
||||
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*",)
|
||||
except ObjectDoesNotExist:
|
||||
user_notifications_count = AnalyticsPath.objects.create(ignore_path=r"^\/user_notifications_count\/.*")
|
||||
|
||||
try:
|
||||
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*")
|
||||
except ObjectDoesNotExist:
|
||||
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
|
||||
|
||||
try:
|
||||
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*")
|
||||
except ObjectDoesNotExist:
|
||||
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
|
||||
|
||||
token.type = 'GA-V4'
|
||||
token.token = 'G-6LYSMYK8DE'
|
||||
token.secret = 'KLlpjLZ-SRGozS5f5wb_kw'
|
||||
token.send_page_views = original_send_page_views
|
||||
token.send_celery_tasks = original_send_celery_tasks
|
||||
token.send_stats = original_send_stats
|
||||
token.name = 'AA Team Public Google Analytics (V4)'
|
||||
token.save()
|
||||
token.ignore_paths.add(admin, user_notifications_count, account_activate)
|
||||
token.save()
|
||||
|
||||
|
||||
def remove_aa_team_token(apps, schema_editor):
|
||||
# Have to define some code to remove this identifier
|
||||
# In case of migration rollback?
|
||||
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||
token = Tokens.objects.filter(token="G-6LYSMYK8DE").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('analytics', '0007_analyticstokens_secret'),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(
|
||||
add_aa_team_token, remove_aa_team_token)]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.0.10 on 2023-05-08 05:24
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('analytics', '0008_add_AA_GA-4_Team_Token '),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='analyticstokens',
|
||||
name='ignore_paths',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='analyticstokens',
|
||||
name='send_celery_tasks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='analyticstokens',
|
||||
name='send_page_views',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AnalyticsPath',
|
||||
),
|
||||
]
|
||||
@@ -7,22 +7,19 @@ from uuid import uuid4
|
||||
|
||||
class AnalyticsIdentifier(models.Model):
|
||||
|
||||
identifier = models.UUIDField(default=uuid4,
|
||||
editable=False)
|
||||
identifier = models.UUIDField(
|
||||
default=uuid4,
|
||||
editable=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk and AnalyticsIdentifier.objects.exists():
|
||||
# Force a single object
|
||||
raise ValidationError('There is can be only one \
|
||||
AnalyticsIdentifier instance')
|
||||
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
|
||||
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class AnalyticsPath(models.Model):
|
||||
ignore_path = models.CharField(max_length=254, default="/example/", help_text="Regex Expression, If matched no Analytics Page View is sent")
|
||||
|
||||
|
||||
class AnalyticsTokens(models.Model):
|
||||
|
||||
class Analytics_Type(models.TextChoices):
|
||||
@@ -32,7 +29,5 @@ class AnalyticsTokens(models.Model):
|
||||
name = models.CharField(max_length=254)
|
||||
type = models.CharField(max_length=254, choices=Analytics_Type.choices)
|
||||
token = models.CharField(max_length=254, blank=False)
|
||||
send_page_views = models.BooleanField(default=False)
|
||||
send_celery_tasks = models.BooleanField(default=False)
|
||||
secret = models.CharField(max_length=254, blank=True)
|
||||
send_stats = models.BooleanField(default=False)
|
||||
ignore_paths = models.ManyToManyField(AnalyticsPath, blank=True)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import logging
|
||||
from celery.signals import task_failure, task_success
|
||||
from django.conf import settings
|
||||
from allianceauth.analytics.tasks import analytics_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@task_failure.connect
|
||||
def process_failure_signal(
|
||||
exception, traceback,
|
||||
sender, task_id, signal,
|
||||
args, kwargs, einfo, **kw):
|
||||
logger.debug("Celery task_failure signal %s" % sender.__class__.__name__)
|
||||
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||
return
|
||||
|
||||
category = sender.__module__
|
||||
|
||||
if 'allianceauth.analytics' not in category:
|
||||
if category.endswith(".tasks"):
|
||||
category = category[:-6]
|
||||
|
||||
action = sender.__name__
|
||||
|
||||
label = f"{exception.__class__.__name__}"
|
||||
|
||||
analytics_event(category=category,
|
||||
action=action,
|
||||
label=label)
|
||||
|
||||
|
||||
@task_success.connect
|
||||
def celery_success_signal(sender, result=None, **kw):
|
||||
logger.debug("Celery task_success signal %s" % sender.__class__.__name__)
|
||||
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||
return
|
||||
|
||||
category = sender.__module__
|
||||
|
||||
if 'allianceauth.analytics' not in category:
|
||||
if category.endswith(".tasks"):
|
||||
category = category[:-6]
|
||||
|
||||
action = sender.__name__
|
||||
label = "Success"
|
||||
|
||||
value = 0
|
||||
if isinstance(result, int):
|
||||
value = result
|
||||
|
||||
analytics_event(category=category,
|
||||
action=action,
|
||||
label=label,
|
||||
value=value)
|
||||
@@ -3,23 +3,24 @@ import logging
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
from celery import shared_task
|
||||
from allianceauth import __version__
|
||||
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||
from .utils import (
|
||||
install_stat_addons,
|
||||
install_stat_tokens,
|
||||
install_stat_users)
|
||||
|
||||
from allianceauth import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_URL = "https://www.google-analytics.com/"
|
||||
BASE_URL = "https://www.google-analytics.com"
|
||||
|
||||
DEBUG_URL = f"{BASE_URL}debug/collect"
|
||||
COLLECTION_URL = f"{BASE_URL}collect"
|
||||
DEBUG_URL = f"{BASE_URL}/debug/mp/collect"
|
||||
COLLECTION_URL = f"{BASE_URL}/mp/collect"
|
||||
|
||||
if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
|
||||
# Force sending of analytics data during in a debug/test environemt
|
||||
# Usefull for developers working on this feature.
|
||||
# Force sending of analytics data during in a debug/test environment
|
||||
# Useful for developers working on this feature.
|
||||
logger.warning(
|
||||
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
|
||||
"This debug instance will send analytics data!")
|
||||
@@ -31,40 +32,38 @@ if settings.DEBUG is True:
|
||||
ANALYTICS_URL = DEBUG_URL
|
||||
|
||||
|
||||
def analytics_event(category: str,
|
||||
action: str,
|
||||
label: str,
|
||||
value: int = 0,
|
||||
def analytics_event(namespace: str,
|
||||
task: str,
|
||||
label: str = "",
|
||||
result: str = "",
|
||||
value: int = 1,
|
||||
event_type: str = 'Celery'):
|
||||
"""
|
||||
Send a Google Analytics Event for each token stored
|
||||
Includes check for if its enabled/disabled
|
||||
|
||||
Args:
|
||||
`category` (str): Celery Namespace
|
||||
`action` (str): Task Name
|
||||
`label` (str): Optional, Task Success/Exception
|
||||
`value` (int): Optional, If bulk, Query size, can be a binary True/False
|
||||
`namespace` (str): Celery Namespace
|
||||
`task` (str): Task Name
|
||||
`label` (str): Optional, additional task label
|
||||
`result` (str): Optional, Task Success/Exception
|
||||
`value` (int): Optional, If bulk, Query size, can be a Boolean
|
||||
`event_type` (str): Optional, Celery or Stats only, Default to Celery
|
||||
"""
|
||||
analyticstokens = AnalyticsTokens.objects.all()
|
||||
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
|
||||
for token in analyticstokens:
|
||||
if event_type == 'Celery':
|
||||
allowed = token.send_celery_tasks
|
||||
elif event_type == 'Stats':
|
||||
for token in AnalyticsTokens.objects.filter(type='GA-V4'):
|
||||
if event_type == 'Stats':
|
||||
allowed = token.send_stats
|
||||
else:
|
||||
allowed = False
|
||||
|
||||
if allowed is True:
|
||||
tracking_id = token.token
|
||||
send_ga_tracking_celery_event.s(
|
||||
tracking_id=tracking_id,
|
||||
client_id=client_id,
|
||||
category=category,
|
||||
action=action,
|
||||
measurement_id=token.token,
|
||||
secret=token.secret,
|
||||
namespace=namespace,
|
||||
task=task,
|
||||
label=label,
|
||||
result=result,
|
||||
value=value).apply_async(priority=9)
|
||||
|
||||
|
||||
@@ -72,136 +71,131 @@ def analytics_event(category: str,
|
||||
def analytics_daily_stats():
|
||||
"""Celery Task: Do not call directly
|
||||
|
||||
Gathers a series of daily statistics and sends analytics events containing them
|
||||
Gathers a series of daily statistics
|
||||
Sends analytics events containing them
|
||||
"""
|
||||
users = install_stat_users()
|
||||
tokens = install_stat_tokens()
|
||||
addons = install_stat_addons()
|
||||
logger.debug("Running Daily Analytics Upload")
|
||||
|
||||
analytics_event(category='allianceauth.analytics',
|
||||
action='send_install_stats',
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_install_stats',
|
||||
label='existence',
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
analytics_event(category='allianceauth.analytics',
|
||||
action='send_install_stats',
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_install_stats',
|
||||
label='users',
|
||||
value=users,
|
||||
event_type='Stats')
|
||||
analytics_event(category='allianceauth.analytics',
|
||||
action='send_install_stats',
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_install_stats',
|
||||
label='tokens',
|
||||
value=tokens,
|
||||
event_type='Stats')
|
||||
analytics_event(category='allianceauth.analytics',
|
||||
action='send_install_stats',
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_install_stats',
|
||||
label='addons',
|
||||
value=addons,
|
||||
event_type='Stats')
|
||||
|
||||
for appconfig in apps.get_app_configs():
|
||||
analytics_event(category='allianceauth.analytics',
|
||||
action='send_extension_stats',
|
||||
label=appconfig.label,
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
|
||||
|
||||
@shared_task()
|
||||
def send_ga_tracking_web_view(
|
||||
tracking_id: str,
|
||||
client_id: str,
|
||||
page: str,
|
||||
title: str,
|
||||
locale: str,
|
||||
useragent: str) -> requests.Response:
|
||||
|
||||
"""Celery Task: Do not call directly
|
||||
|
||||
Sends Page View events to GA, Called only via analytics.middleware
|
||||
|
||||
Parameters
|
||||
----------
|
||||
`tracking_id` (str): Unique Server Identifier
|
||||
`client_id` (str): GA Token
|
||||
`page` (str): Page Path
|
||||
`title` (str): Page Title
|
||||
`locale` (str): Browser Language
|
||||
`useragent` (str): Browser UserAgent
|
||||
|
||||
Returns
|
||||
-------
|
||||
requests.Reponse Object
|
||||
"""
|
||||
headers = {"User-Agent": useragent}
|
||||
|
||||
payload = {
|
||||
'v': '1',
|
||||
'tid': tracking_id,
|
||||
'cid': client_id,
|
||||
't': 'pageview',
|
||||
'dp': page,
|
||||
'dt': title,
|
||||
'ul': locale,
|
||||
'ua': useragent,
|
||||
'aip': 1,
|
||||
'an': "allianceauth",
|
||||
'av': __version__
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
ANALYTICS_URL, data=payload,
|
||||
timeout=5, headers=headers)
|
||||
logger.debug(f"Analytics Page View HTTP{response.status_code}")
|
||||
return response
|
||||
if appconfig.label in [
|
||||
"django_celery_beat",
|
||||
"bootstrapform",
|
||||
"messages",
|
||||
"sessions",
|
||||
"auth",
|
||||
"staticfiles",
|
||||
"users",
|
||||
"addons",
|
||||
"admin",
|
||||
"humanize",
|
||||
"contenttypes",
|
||||
"sortedm2m",
|
||||
"django_bootstrap5",
|
||||
"tokens",
|
||||
"authentication",
|
||||
"services",
|
||||
"framework",
|
||||
"notifications"
|
||||
"eveonline",
|
||||
"navhelper",
|
||||
"analytics",
|
||||
"menu",
|
||||
"theme"
|
||||
]:
|
||||
pass
|
||||
else:
|
||||
analytics_event(namespace='allianceauth.analytics',
|
||||
task='send_extension_stats',
|
||||
label=appconfig.label,
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
|
||||
|
||||
@shared_task()
|
||||
def send_ga_tracking_celery_event(
|
||||
tracking_id: str,
|
||||
client_id: str,
|
||||
category: str,
|
||||
action: str,
|
||||
label: str,
|
||||
value: int) -> requests.Response:
|
||||
measurement_id: str,
|
||||
secret: str,
|
||||
namespace: str,
|
||||
task: str,
|
||||
label: str = "",
|
||||
result: str = "",
|
||||
value: int = 1):
|
||||
"""Celery Task: Do not call directly
|
||||
|
||||
Sends Page View events to GA, Called only via analytics.middleware
|
||||
Sends an events to GA
|
||||
|
||||
Parameters
|
||||
----------
|
||||
`tracking_id` (str): Unique Server Identifier
|
||||
`client_id` (str): GA Token
|
||||
`category` (str): Celery Namespace
|
||||
`action` (str): Task Name
|
||||
`label` (str): Optional, Task Success/Exception
|
||||
`measurement_id` (str): GA Token
|
||||
`secret` (str): GA Authentication Secret
|
||||
`namespace` (str): Celery Namespace
|
||||
`task` (str): Task Name
|
||||
`label` (str): Optional, additional task label
|
||||
`result` (str): Optional, Task Success/Exception
|
||||
`value` (int): Optional, If bulk, Query size, can be a binary True/False
|
||||
|
||||
Returns
|
||||
-------
|
||||
requests.Reponse Object
|
||||
"""
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"}
|
||||
parameters = {
|
||||
'measurement_id': measurement_id,
|
||||
'api_secret': secret
|
||||
}
|
||||
|
||||
payload = {
|
||||
'v': '1',
|
||||
'tid': tracking_id,
|
||||
'cid': client_id,
|
||||
't': 'event',
|
||||
'ec': category,
|
||||
'ea': action,
|
||||
'el': label,
|
||||
'ev': value,
|
||||
'aip': 1,
|
||||
'an': "allianceauth",
|
||||
'av': __version__
|
||||
'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex,
|
||||
"user_properties": {
|
||||
"allianceauth_version": {
|
||||
"value": __version__
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
ANALYTICS_URL, data=payload,
|
||||
timeout=5, headers=headers)
|
||||
logger.debug(f"Analytics Celery/Stats Event HTTP{response.status_code}")
|
||||
return response
|
||||
},
|
||||
'non_personalized_ads': True,
|
||||
"events": [{
|
||||
"name": "celery_event",
|
||||
"params": {
|
||||
"namespace": namespace,
|
||||
"task": task,
|
||||
'result': result,
|
||||
'label': label,
|
||||
"value": value
|
||||
}
|
||||
}]
|
||||
}
|
||||
try:
|
||||
response = requests.post(
|
||||
ANALYTICS_URL,
|
||||
params=parameters,
|
||||
json=payload,
|
||||
timeout=10)
|
||||
response.raise_for_status()
|
||||
logger.debug(
|
||||
f"Analytics Celery/Stats Event HTTP{response.status_code}")
|
||||
return response.status_code
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.debug(e)
|
||||
return response.status_code
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.debug(e)
|
||||
return "Failed"
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import requests_mock
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
from allianceauth.analytics.tasks import ANALYTICS_URL
|
||||
from allianceauth.eveonline.tasks import update_character
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@requests_mock.mock()
|
||||
class TestAnalyticsForViews(NoSocketsTestCase):
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
def test_should_run_analytics(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
self.client.force_login(user)
|
||||
# when
|
||||
response = self.client.get("/dashboard/")
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(requests_mocker.called)
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=True)
|
||||
def test_should_not_run_analytics(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
self.client.force_login(user)
|
||||
# when
|
||||
response = self.client.get("/dashboard/")
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(requests_mocker.called)
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@requests_mock.mock()
|
||||
class TestAnalyticsForTasks(NoSocketsTestCase):
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_run_analytics_for_successful_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertTrue(requests_mocker.called)
|
||||
payload = parse_qs(requests_mocker.last_request.text)
|
||||
self.assertListEqual(payload["el"], ["Success"])
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=True)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_not_run_analytics_for_successful_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertFalse(requests_mocker.called)
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_run_analytics_for_failed_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
mock_update_character.side_effect = RuntimeError
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertTrue(requests_mocker.called)
|
||||
payload = parse_qs(requests_mocker.last_request.text)
|
||||
self.assertNotEqual(payload["el"], ["Success"])
|
||||
|
||||
@override_settings(ANALYTICS_DISABLED=True)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_not_run_analytics_for_failed_task(
|
||||
self, requests_mocker, mock_update_character
|
||||
):
|
||||
# given
|
||||
requests_mocker.post(ANALYTICS_URL)
|
||||
mock_update_character.side_effect = RuntimeError
|
||||
user = AuthUtils.create_user("Bruce Wayne")
|
||||
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||
# when
|
||||
update_character.delay(character.character_id)
|
||||
# then
|
||||
self.assertTrue(mock_update_character.called)
|
||||
self.assertFalse(requests_mocker.called)
|
||||
@@ -1,24 +0,0 @@
|
||||
from allianceauth.analytics.middleware import AnalyticsMiddleware
|
||||
from unittest.mock import Mock
|
||||
from django.http import HttpResponse
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
|
||||
class TestAnalyticsMiddleware(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = AnalyticsMiddleware(HttpResponse)
|
||||
self.request = Mock()
|
||||
self.request.headers = {
|
||||
"User-Agent": "AUTOMATED TEST"
|
||||
}
|
||||
self.request.path = '/testURL/'
|
||||
self.request.session = {}
|
||||
self.request.LANGUAGE_CODE = 'en'
|
||||
self.response = Mock()
|
||||
self.response.content = 'hello world'
|
||||
|
||||
def test_middleware(self):
|
||||
response = self.middleware.process_response(self.request, self.response)
|
||||
self.assertEqual(self.response, response)
|
||||
@@ -23,4 +23,5 @@ class TestAnalyticsIdentifier(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
AnalyticsIdentifier.objects.create(identifier=uuid_2)
|
||||
self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
|
||||
self.assertEqual(AnalyticsIdentifier.objects.get(pk=1).identifier, UUID(uuid_1))
|
||||
self.assertEqual(AnalyticsIdentifier.objects.get(
|
||||
pk=1).identifier, UUID(uuid_1))
|
||||
|
||||
@@ -4,12 +4,11 @@ from django.test.utils import override_settings
|
||||
|
||||
from allianceauth.analytics.tasks import (
|
||||
analytics_event,
|
||||
send_ga_tracking_celery_event,
|
||||
send_ga_tracking_web_view)
|
||||
send_ga_tracking_celery_event)
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
|
||||
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/collect'
|
||||
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/mp/collect'
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
@@ -18,195 +17,53 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
||||
def test_analytics_event(self, requests_mocker):
|
||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||
analytics_event(
|
||||
category='allianceauth.analytics',
|
||||
action='send_tests',
|
||||
label='test',
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
|
||||
def test_send_ga_tracking_web_view_sent(self, requests_mocker):
|
||||
"""This test sends if the event SENDS to google.
|
||||
Not if it was successful.
|
||||
"""
|
||||
# given
|
||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||
tracking_id = 'UA-186249766-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
page = '/index/'
|
||||
title = 'Hello World'
|
||||
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"
|
||||
# when
|
||||
response = send_ga_tracking_web_view(
|
||||
tracking_id,
|
||||
client_id,
|
||||
page,
|
||||
title,
|
||||
locale,
|
||||
useragent)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
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'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
page = '/index/'
|
||||
title = 'Hello World'
|
||||
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"
|
||||
# when
|
||||
json_response = send_ga_tracking_web_view(
|
||||
tracking_id,
|
||||
client_id,
|
||||
page,
|
||||
title,
|
||||
locale,
|
||||
useragent).json()
|
||||
# then
|
||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||
|
||||
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'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
page = '/index/'
|
||||
title = 'Hello World'
|
||||
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"
|
||||
# when
|
||||
json_response = send_ga_tracking_web_view(
|
||||
tracking_id,
|
||||
client_id,
|
||||
page,
|
||||
title,
|
||||
locale,
|
||||
useragent).json()
|
||||
# then
|
||||
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."
|
||||
)
|
||||
|
||||
# [{'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'}]
|
||||
namespace='allianceauth.analytics',
|
||||
task='send_tests',
|
||||
label='test',
|
||||
value=1,
|
||||
result="Success",
|
||||
event_type='Stats')
|
||||
|
||||
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'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
token = 'G-6LYSMYK8DE'
|
||||
secret = 'KLlpjLZ-SRGozS5f5wb_kw',
|
||||
category = 'test'
|
||||
action = 'test'
|
||||
label = 'test'
|
||||
value = '1'
|
||||
# when
|
||||
response = send_ga_tracking_celery_event(
|
||||
tracking_id,
|
||||
client_id,
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
value)
|
||||
task = send_ga_tracking_celery_event(
|
||||
token,
|
||||
secret,
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
value)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(task, 200)
|
||||
|
||||
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}]}
|
||||
json={"validationMessages": []}
|
||||
)
|
||||
tracking_id = 'UA-186249766-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
token = 'G-6LYSMYK8DE'
|
||||
secret = 'KLlpjLZ-SRGozS5f5wb_kw',
|
||||
category = 'test'
|
||||
action = 'test'
|
||||
label = 'test'
|
||||
value = '1'
|
||||
# when
|
||||
json_response = send_ga_tracking_celery_event(
|
||||
tracking_id,
|
||||
client_id,
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
value).json()
|
||||
task = send_ga_tracking_celery_event(
|
||||
token,
|
||||
secret,
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
value)
|
||||
# then
|
||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||
|
||||
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'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
category = 'test'
|
||||
action = 'test'
|
||||
label = 'test'
|
||||
value = '1'
|
||||
# when
|
||||
json_response = send_ga_tracking_celery_event(
|
||||
tracking_id,
|
||||
client_id,
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
value).json()
|
||||
# then
|
||||
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.assertTrue(task, 200)
|
||||
|
||||
@@ -18,17 +18,17 @@ def create_testdata():
|
||||
'abc@example.com',
|
||||
'password'
|
||||
)
|
||||
#Token.objects.all().delete()
|
||||
#Token.objects.create(
|
||||
# Token.objects.all().delete()
|
||||
# Token.objects.create(
|
||||
# character_id=101,
|
||||
# character_name='character1',
|
||||
# access_token='my_access_token'
|
||||
#)
|
||||
#Token.objects.create(
|
||||
# )
|
||||
# Token.objects.create(
|
||||
# character_id=102,
|
||||
# character_name='character2',
|
||||
# access_token='my_access_token'
|
||||
#)
|
||||
# )
|
||||
|
||||
|
||||
class TestAnalyticsUtils(TestCase):
|
||||
@@ -40,7 +40,7 @@ class TestAnalyticsUtils(TestCase):
|
||||
users = install_stat_users()
|
||||
self.assertEqual(users, expected)
|
||||
|
||||
#def test_install_stat_tokens(self):
|
||||
# def test_install_stat_tokens(self):
|
||||
# create_testdata()
|
||||
# expected = 2
|
||||
#
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
from django.core.checks import Warning, Error, register
|
||||
|
||||
|
||||
class AllianceAuthConfig(AppConfig):
|
||||
name = 'allianceauth'
|
||||
|
||||
def ready(self) -> None:
|
||||
import allianceauth.checks # noqa
|
||||
|
||||
@@ -288,7 +288,7 @@ class UserAdmin(BaseUserAdmin):
|
||||
Behavior of groups and characters columns can be configured via settings
|
||||
"""
|
||||
|
||||
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
||||
inlines = [UserProfileInline]
|
||||
ordering = ('username', )
|
||||
list_select_related = ('profile__state', 'profile__main_character')
|
||||
show_full_result_count = True
|
||||
|
||||
59
allianceauth/authentication/auth_hooks.py
Normal file
59
allianceauth/authentication/auth_hooks.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from allianceauth.hooks import DashboardItemHook
|
||||
from allianceauth import hooks
|
||||
from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin
|
||||
|
||||
|
||||
class UserCharactersHook(DashboardItemHook):
|
||||
def __init__(self):
|
||||
DashboardItemHook.__init__(
|
||||
self,
|
||||
dashboard_characters,
|
||||
5
|
||||
)
|
||||
|
||||
|
||||
class UserGroupsHook(DashboardItemHook):
|
||||
def __init__(self):
|
||||
DashboardItemHook.__init__(
|
||||
self,
|
||||
dashboard_groups,
|
||||
5
|
||||
)
|
||||
|
||||
|
||||
class AdminHook(DashboardItemHook):
|
||||
def __init__(self):
|
||||
DashboardItemHook.__init__(
|
||||
self,
|
||||
dashboard_admin,
|
||||
1
|
||||
)
|
||||
|
||||
|
||||
class ESICheckHook(DashboardItemHook):
|
||||
def __init__(self):
|
||||
DashboardItemHook.__init__(
|
||||
self,
|
||||
dashboard_esi_check,
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
@hooks.register('dashboard_hook')
|
||||
def register_character_hook():
|
||||
return UserCharactersHook()
|
||||
|
||||
|
||||
@hooks.register('dashboard_hook')
|
||||
def register_groups_hook():
|
||||
return UserGroupsHook()
|
||||
|
||||
|
||||
@hooks.register('dashboard_hook')
|
||||
def register_admin_hook():
|
||||
return AdminHook()
|
||||
|
||||
|
||||
@hooks.register('dashboard_hook')
|
||||
def register_esi_hook():
|
||||
return ESICheckHook()
|
||||
12
allianceauth/authentication/constants.py
Normal file
12
allianceauth/authentication/constants.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Overide ESI messages in the dashboard widget
|
||||
# when the returned messages are not helpful or out of date
|
||||
ESI_ERROR_MESSAGE_OVERRIDES = {
|
||||
420: _("This software has exceeded the error limit for ESI. "
|
||||
"If you are a user, please contact the maintainer of this software."
|
||||
" If you are a developer/maintainer, please make a greater "
|
||||
"effort in the future to receive valid responses. For tips on how, "
|
||||
"come have a chat with us in ##3rd-party-dev-and-esi on the EVE "
|
||||
"Online Discord. https://www.eveonline.com/discord")
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
from django.urls import include
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from functools import wraps
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class UserSettingsMiddleware(MiddlewareMixin):
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
# AA v3 NIGHT_MODE
|
||||
# Set our Night mode flag from the DB
|
||||
# Null = hasnt been set by the user ever, dont act.
|
||||
#
|
||||
@@ -42,4 +43,13 @@ class UserSettingsMiddleware(MiddlewareMixin):
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
# AA v4 Themes
|
||||
# Null = has not been set by the user ever, dont act
|
||||
# DEFAULT_THEME or DEFAULT_THEME_DARK will be used in get_theme()
|
||||
try:
|
||||
if request.user.profile.theme is not None:
|
||||
request.session["THEME"] = request.user.profile.theme
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
return response
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.10 on 2023-10-07 07:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0021_alter_userprofile_language'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='theme',
|
||||
field=models.CharField(blank=True, help_text='Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps', max_length=200, null=True, verbose_name='Theme'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.13 on 2024-05-12 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0022_userprofile_theme'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userprofile',
|
||||
name='language',
|
||||
field=models.CharField(blank=True, choices=[('en', 'English'), ('de', 'German'), ('es', 'Spanish'), ('zh-hans', 'Chinese Simplified'), ('ru', 'Russian'), ('ko', 'Korean'), ('fr', 'French'), ('ja', 'Japanese'), ('it', 'Italian'), ('uk', 'Ukrainian'), ('pl', 'Polish')], default='', max_length=10, verbose_name='Language'),
|
||||
),
|
||||
]
|
||||
@@ -78,6 +78,7 @@ class UserProfile(models.Model):
|
||||
JAPANESE = 'ja', _('Japanese')
|
||||
ITALIAN = 'it', _('Italian')
|
||||
UKRAINIAN = 'uk', _('Ukrainian')
|
||||
POLISH = 'pl', _("Polish")
|
||||
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
@@ -101,6 +102,13 @@ class UserProfile(models.Model):
|
||||
_("Night Mode"),
|
||||
blank=True,
|
||||
null=True)
|
||||
theme = models.CharField(
|
||||
_("Theme"),
|
||||
max_length=200,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps"
|
||||
)
|
||||
|
||||
def assign_state(self, state=None, commit=True):
|
||||
if not state:
|
||||
|
||||
@@ -1,190 +1,15 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Dashboard" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1 class="page-header text-center">{% translate "Dashboard" %}</h1>
|
||||
{% if user.is_staff %}
|
||||
{% include 'allianceauth/admin-status/include.html' %}
|
||||
{% endif %}
|
||||
<div class="col-sm-12">
|
||||
<div class="row vertical-flexbox-row2">
|
||||
<div class="col-sm-6 text-center">
|
||||
<div class="panel panel-primary" style="height:100%">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% blocktranslate with state=request.user.profile.state %}
|
||||
Main Character (State: {{ state }})
|
||||
{% endblocktranslate %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if request.user.profile.main_character %}
|
||||
{% with request.user.profile.main_character as main %}
|
||||
<div class="hidden-xs">
|
||||
<div class="col-lg-4 col-sm-2">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar" src="{{ main.portrait_url_128 }}" alt="{{ main.character_name }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">{{ main.character_name }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-4 col-sm-2">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar" src="{{ main.corporation_logo_url_128 }}" alt="{{ main.corporation_name }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">{{ main.corporation_name }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-4 col-sm-2">
|
||||
{% if main.alliance_id %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar" src="{{ main.alliance_logo_url_128 }}" alt="{{ main.alliance_name }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">{{ main.alliance_name }}</td>
|
||||
<tr>
|
||||
</table>
|
||||
{% elif main.faction_id %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar" src="{{ main.faction_logo_url_128 }}" alt="{{ main.faction_name }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">{{ main.faction_name }}</td>
|
||||
<tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="table visible-xs-block">
|
||||
<p>
|
||||
<img class="ra-avatar" src="{{ main.portrait_url_64 }}" alt="{{ main.corporation_name }}">
|
||||
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}" alt="{{ main.corporation_name }}">
|
||||
{% if main.alliance_id %}
|
||||
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}" alt="{{ main.alliance_name }}">
|
||||
{% endif %}
|
||||
{% if main.faction_id %}
|
||||
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}" alt="{{ main.faction_name }}">
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ main.character_name }}</strong><br>
|
||||
{{ main.corporation_name }}<br>
|
||||
{% if main.alliance_id %}
|
||||
{{ main.alliance_name }}<br>
|
||||
{% endif %}
|
||||
{% if main.faction_id %}
|
||||
{{ main.faction_name }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% translate "No main character set." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
||||
title="Add Character">{% translate 'Add Character' %}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
|
||||
title="Change Main Character">{% translate "Change Main" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 text-center">
|
||||
<div class="panel panel-success" style="height:100%">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% translate "Group Memberships" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div style="height: 240px;overflow-y:auto;">
|
||||
<table class="table table-aa">
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>{{ group.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title text-center" style="text-align: center">
|
||||
{% translate 'Characters' %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-aa hidden-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% translate 'Name' %}</th>
|
||||
<th class="text-center">{% translate 'Corp' %}</th>
|
||||
<th class="text-center">{% translate 'Alliance' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for char in characters %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}" alt="{{ 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.alliance_name|default:"" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-aa visible-xs-block" style="width: 100%">
|
||||
<tbody>
|
||||
{% for char in characters %}
|
||||
<tr>
|
||||
<td class="text-center" style="vertical-align: middle">
|
||||
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
|
||||
</td>
|
||||
<td class="text-center" style="vertical-align: middle; width: 100%">
|
||||
<strong>{{ char.character_name }}</strong><br>
|
||||
{{ char.corporation_name }}<br>
|
||||
{{ char.alliance_name|default:"" }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% for dash in views %}
|
||||
{{ dash | safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends 'allianceauth/base.html' %}
|
||||
|
||||
|
||||
{% block page_title %}Dashboard{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<h1>Dashboard Dummy</h1>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% load i18n %}
|
||||
<div id="aa-dashboard-panel-characters" class="col-12 col-xl-8 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
{% translate "Characters" as widget_title %}
|
||||
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
||||
|
||||
<div>
|
||||
<div style="height: 300px; overflow-y:auto;">
|
||||
<div class="d-flex">
|
||||
<a href="{% url 'authentication:add_character' %}" class="btn btn-primary flex-fill m-1" title="{% translate 'Add Character' %}">
|
||||
<span class="d-md-inline m-2">{% translate 'Add Character' %}</span>
|
||||
</a>
|
||||
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-primary flex-fill m-1" title="{% translate 'Change Main' %}">
|
||||
<span class="d-md-inline m-2">{% translate 'Change Main' %}</span>
|
||||
</a>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% translate "Name" %}</th>
|
||||
<th class="text-center">{% translate "Corp" %}</th>
|
||||
<th class="text-center">{% translate "Alliance" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for char in characters %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar rounded-circle" src="{{ char.portrait_url_32 }}" alt="{{ 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.alliance_name|default_if_none:"" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
<div id="aa-dashboard-panel-membership" class="col-12 col-xl-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
{% translate "Membership" as widget_title %}
|
||||
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
||||
|
||||
<div>
|
||||
<div style="height: 300px; overflow-y:auto;">
|
||||
<h5 class="text-center">{% translate "State:" %} {{ request.user.profile.state }}</h5>
|
||||
<table class="table">
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td class="text-center">{{ group.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,62 +1,85 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
|
||||
{% block page_title %}
|
||||
{% translate "Token Management" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Token Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% 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%">
|
||||
<div>
|
||||
<p class="mb-3">
|
||||
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
|
||||
</p>
|
||||
|
||||
<table class="table w-100" id="table_tokens">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Scopes" %}</th>
|
||||
<th class="text-right">{% translate "Actions" %}</th>
|
||||
<th class="text-end">{% 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>
|
||||
<td style="white-space:initial;">
|
||||
{% for s in t.scopes.all %}
|
||||
<span class="badge bg-secondary">{{ s.name }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
<td nowrap class="text-end">
|
||||
<a href="{% url 'authentication:token_delete' t.id %}" class="btn btn-danger"><i class="fa-solid fa-trash-can"></i></a>
|
||||
<a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fa-solid fa-rotate"></i></a>
|
||||
</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 %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
{% endblock %}
|
||||
{% include "bundles/datatables-js-bs5.html" %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
let grp = 2;
|
||||
|
||||
const 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((group, i) => {
|
||||
if (last !== group) {
|
||||
$(rows).eq(i).before(`<tr class="h5 table-primary"><td colspan="3">${group}</td></tr>`);
|
||||
|
||||
last = group;
|
||||
}
|
||||
});
|
||||
},
|
||||
'stateSave': true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css.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 %}
|
||||
{% include "bundles/datatables-css-bs5.html" %}
|
||||
{% endblock extra_css %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load theme_tags %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -7,6 +8,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<!-- TODO Bundle all the site specific stuff up into its own template for easy override -->
|
||||
<meta property="og:title" content="{{ SITE_NAME }}">
|
||||
<meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
|
||||
@@ -15,8 +17,9 @@
|
||||
|
||||
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
|
||||
|
||||
{% include 'bundles/bootstrap-css.html' %}
|
||||
{% theme_css %}
|
||||
{% include 'bundles/fontawesome.html' %}
|
||||
|
||||
{% block extra_include %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,25 +32,23 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.panel-transparent {
|
||||
background: rgba(48, 48, 48, 0.7);
|
||||
color: #ffffff;
|
||||
.card-login {
|
||||
background: rgba(48 48 48 / 0.7);
|
||||
color: rgb(255 255 255);
|
||||
padding-bottom: 21px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
|
||||
}
|
||||
|
||||
#lang-select {
|
||||
width: 40%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
{% block extra_style %}
|
||||
{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container" style="margin-top:150px;">
|
||||
{% block content %}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="dropdown">
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||
|
||||
<select class="form-select" onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
|
||||
{% for language in languages %}
|
||||
<option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
{{ language.name_local|capfirst }} ({{ language.code }})
|
||||
|
||||
@@ -3,39 +3,41 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="panel panel-default panel-transparent">
|
||||
<div class="panel-body">
|
||||
<div class="col-md-12">
|
||||
{% block middle_box_content %}
|
||||
{% endblock %}
|
||||
<div class="card card-login border-secondary p-3">
|
||||
<div class="card-body">
|
||||
<div class="text-center">
|
||||
{% block middle_box_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'public/lang_select.html' %}
|
||||
|
||||
<p class="text-center mt-3">
|
||||
{% translate "For information on SSO, ESI and security read the CCP Dev Blog" %}<br>
|
||||
<a class="text-reset" href="https://www.eveonline.com/news/view/introducing-esi" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Introducing ESI - A New API For EVE Online" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
<a class="text-reset" href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Manage ESI Applications" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% include 'public/lang_select.html' %}
|
||||
|
||||
<p class="text-center" style="margin-top: 2rem;">
|
||||
{% translate "For information on SSO, ESI and security read the CCP Dev Blog" %}<br>
|
||||
<a href="https://www.eveonline.com/article/introducing-esi" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Introducing ESI - A New API For Eve Online" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
<a href="https://community.eveonline.com/support/third-party-applications/" target="_blank" rel="noopener noreferrer">
|
||||
{% translate "Manage ESI Applications" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_include %}
|
||||
{% include 'bundles/bootstrap-js.html' %}
|
||||
{% include 'bundles/bootstrap-js-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
{% extends 'public/base.html' %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Registration" %}{% endblock %}
|
||||
|
||||
{% block extra_include %}
|
||||
{% include 'bundles/bootstrap-css.html' %}
|
||||
{% include 'bundles/bootstrap-css-bs5.html' %}
|
||||
{% include 'bundles/fontawesome.html' %}
|
||||
{% include 'bundles/bootstrap-js.html' %}
|
||||
{% include 'bundles/bootstrap-js-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="panel panel-default panel-transparent">
|
||||
<div class="panel-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form|bootstrap }}
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Register" %}</button>
|
||||
</form>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card card-login border-secondary p-3">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">{% translate "Register" %}</button>
|
||||
</form>
|
||||
|
||||
{% include 'public/lang_select.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'public/lang_select.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends 'public/middle_box.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block middle_box_content %}
|
||||
<div class="alert alert-danger">{% translate 'Invalid or expired activation link.' %}</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import json
|
||||
import requests_mock
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from allianceauth.authentication.views import task_counts
|
||||
from allianceauth.authentication.views import task_counts, esi_check
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.views"
|
||||
|
||||
@@ -21,6 +23,8 @@ class TestRunningTasksCount(TestCase):
|
||||
super().setUpClass()
|
||||
cls.factory = RequestFactory()
|
||||
cls.user = AuthUtils.create_user("bruce_wayne")
|
||||
cls.user.is_superuser = True
|
||||
cls.user.save()
|
||||
|
||||
def test_should_return_data(
|
||||
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||
@@ -35,5 +39,164 @@ class TestRunningTasksCount(TestCase):
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
|
||||
jsonresponse_to_dict(response), {
|
||||
"tasks_running": 2, "tasks_queued": 3}
|
||||
)
|
||||
|
||||
def test_su_only(
|
||||
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||
):
|
||||
self.user.is_superuser = False
|
||||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
# given
|
||||
mock_active_tasks_count.return_value = 2
|
||||
mock_queued_tasks_count.return_value = 3
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = task_counts(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class TestEsiCheck(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.factory = RequestFactory()
|
||||
cls.user = AuthUtils.create_user("bruce_wayne")
|
||||
cls.user.is_superuser = True
|
||||
cls.user.save()
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_401_data_returns_200(
|
||||
self, m
|
||||
):
|
||||
error_json = {
|
||||
"error": "You have been banned from using ESI. Please contact Technical Support. (support@eveonline.com)"
|
||||
}
|
||||
status_code = 401
|
||||
m.get(
|
||||
"https://esi.evetech.net/latest/status/?datasource=tranquility",
|
||||
text=json.dumps(error_json),
|
||||
status_code=status_code
|
||||
)
|
||||
# given
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = esi_check(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {
|
||||
"status": status_code,
|
||||
"data": error_json
|
||||
}
|
||||
)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_504_data_returns_200(
|
||||
self, m
|
||||
):
|
||||
error_json = {
|
||||
"error": "Gateway timeout message",
|
||||
"timeout": 5000
|
||||
}
|
||||
status_code = 504
|
||||
m.get(
|
||||
"https://esi.evetech.net/latest/status/?datasource=tranquility",
|
||||
text=json.dumps(error_json),
|
||||
status_code=status_code
|
||||
)
|
||||
# given
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = esi_check(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {
|
||||
"status": status_code,
|
||||
"data": error_json
|
||||
}
|
||||
)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_420_data_override(
|
||||
self, m
|
||||
):
|
||||
error_json = {
|
||||
"error": "message from CCP",
|
||||
}
|
||||
status_code = 420
|
||||
m.get(
|
||||
"https://esi.evetech.net/latest/status/?datasource=tranquility",
|
||||
text=json.dumps(error_json),
|
||||
status_code=status_code
|
||||
)
|
||||
# given
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = esi_check(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotEqual(
|
||||
jsonresponse_to_dict(response)["data"],
|
||||
error_json
|
||||
)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {
|
||||
"status": status_code,
|
||||
"data": {
|
||||
"error": ESI_ERROR_MESSAGE_OVERRIDES.get(status_code)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_200_data_returns_200(
|
||||
self, m
|
||||
):
|
||||
good_json = {
|
||||
"players": 5,
|
||||
"server_version": "69420",
|
||||
"start_time": "2030-01-01T23:59:59Z"
|
||||
}
|
||||
status_code = 200
|
||||
|
||||
m.get(
|
||||
"https://esi.evetech.net/latest/status/?datasource=tranquility",
|
||||
text=json.dumps(good_json),
|
||||
status_code=status_code
|
||||
)
|
||||
# given
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = esi_check(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {
|
||||
"status": status_code,
|
||||
"data": good_json
|
||||
}
|
||||
)
|
||||
|
||||
def test_su_only(
|
||||
self,
|
||||
):
|
||||
self.user.is_superuser = False
|
||||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
# given
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = esi_check(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@@ -38,5 +38,7 @@ urlpatterns = [
|
||||
name='token_refresh'
|
||||
),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
|
||||
path('task-counts/', views.task_counts, name='task_counts'),
|
||||
path('esi-check/', views.esi_check, name='esi_check'),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django_registration.backends.activation.views import (
|
||||
REGISTRATION_SALT, ActivationView as BaseActivationView,
|
||||
RegistrationView as BaseRegistrationView,
|
||||
@@ -9,7 +10,7 @@ from django_registration.signals import user_registered
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import signing
|
||||
from django.http import JsonResponse
|
||||
@@ -22,14 +23,16 @@ from esi.decorators import token_required
|
||||
from esi.models import Token
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.hooks import get_hooks
|
||||
|
||||
from .constants import ESI_ERROR_MESSAGE_OVERRIDES
|
||||
from .core.celery_workers import active_tasks_count, queued_tasks_count
|
||||
from .forms import RegistrationForm
|
||||
from .models import CharacterOwnership
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
_has_auto_groups = True
|
||||
from allianceauth.eveonline.autogroups.models import *
|
||||
from allianceauth.eveonline.autogroups.models import * # noqa: F401, F403
|
||||
else:
|
||||
_has_auto_groups = False
|
||||
|
||||
@@ -42,23 +45,58 @@ def index(request):
|
||||
return redirect('authentication:dashboard')
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
def dashboard_groups(request):
|
||||
groups = request.user.groups.all()
|
||||
if _has_auto_groups:
|
||||
groups = groups\
|
||||
.filter(managedalliancegroup__isnull=True)\
|
||||
.filter(managedcorpgroup__isnull=True)
|
||||
groups = groups.order_by('name')
|
||||
|
||||
context = {
|
||||
'groups': groups,
|
||||
}
|
||||
return render_to_string('authentication/dashboard_groups.html', context=context, request=request)
|
||||
|
||||
|
||||
def dashboard_characters(request):
|
||||
characters = EveCharacter.objects\
|
||||
.filter(character_ownership__user=request.user)\
|
||||
.select_related()\
|
||||
.order_by('character_name')
|
||||
|
||||
context = {
|
||||
'groups': groups,
|
||||
'characters': characters
|
||||
}
|
||||
return render_to_string('authentication/dashboard_characters.html', context=context, request=request)
|
||||
|
||||
|
||||
def dashboard_admin(request):
|
||||
if request.user.is_superuser:
|
||||
return render_to_string('allianceauth/admin-status/include.html', request=request)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def dashboard_esi_check(request):
|
||||
if request.user.is_superuser:
|
||||
return render_to_string('allianceauth/admin-status/esi_check.html', request=request)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
_dash_items = list()
|
||||
hooks = get_hooks('dashboard_hook')
|
||||
items = [fn() for fn in hooks]
|
||||
items.sort(key=lambda i: i.order)
|
||||
for item in items:
|
||||
_dash_items.append(item.render(request))
|
||||
|
||||
context = {
|
||||
'views': _dash_items,
|
||||
}
|
||||
return render(request, 'authentication/dashboard.html', context)
|
||||
|
||||
|
||||
@@ -106,23 +144,30 @@ def token_refresh(request, token_id=None):
|
||||
@login_required
|
||||
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
||||
def main_character_change(request, token):
|
||||
logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}")
|
||||
logger.debug(
|
||||
f"main_character_change called by user {request.user} for character {token.character_name}")
|
||||
try:
|
||||
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
|
||||
co = CharacterOwnership.objects.get(
|
||||
character__character_id=token.character_id, user=request.user)
|
||||
except CharacterOwnership.DoesNotExist:
|
||||
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
|
||||
co = CharacterOwnership.objects.create_by_token(token)
|
||||
else:
|
||||
messages.error(
|
||||
request,
|
||||
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
|
||||
_('Cannot change main character to %(char)s: character owned by a different account.') % (
|
||||
{'char': token.character_name})
|
||||
)
|
||||
co = None
|
||||
if co:
|
||||
request.user.profile.main_character = co.character
|
||||
request.user.profile.save(update_fields=['main_character'])
|
||||
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character})
|
||||
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character}))
|
||||
messages.success(request, _('Changed main character to %s') % co.character)
|
||||
logger.info(
|
||||
'Changed user {user} main character to {char}'.format(
|
||||
user=request.user, char=co.character
|
||||
)
|
||||
)
|
||||
return redirect("authentication:dashboard")
|
||||
|
||||
|
||||
@@ -130,9 +175,11 @@ def main_character_change(request, token):
|
||||
def add_character(request, token):
|
||||
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
||||
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
||||
messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name})))
|
||||
messages.success(request, _(
|
||||
'Added %(name)s to your account.' % ({'name': token.character_name})))
|
||||
else:
|
||||
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
|
||||
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % (
|
||||
{'name': token.character_name})))
|
||||
return redirect('authentication:dashboard')
|
||||
|
||||
|
||||
@@ -175,8 +222,10 @@ def sso_login(request, token):
|
||||
token.delete()
|
||||
messages.error(
|
||||
request,
|
||||
_('Unable to authenticate as the selected character. '
|
||||
'Please log in with the main character associated with this account.')
|
||||
_(
|
||||
'Unable to authenticate as the selected character. '
|
||||
'Please log in with the main character associated with this account.'
|
||||
)
|
||||
)
|
||||
return redirect(settings.LOGIN_URL)
|
||||
|
||||
@@ -249,7 +298,8 @@ class RegistrationView(BaseRegistrationView):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def register(self, form):
|
||||
user = User.objects.get(pk=self.request.session.get('registration_uid'))
|
||||
user = User.objects.get(
|
||||
pk=self.request.session.get('registration_uid'))
|
||||
user.email = form.cleaned_data['email']
|
||||
user_registered.send(self.__class__, user=user, request=self.request)
|
||||
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
||||
@@ -266,7 +316,8 @@ class RegistrationView(BaseRegistrationView):
|
||||
|
||||
def get_email_context(self, activation_key):
|
||||
context = super().get_email_context(activation_key)
|
||||
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
|
||||
context['url'] = context['site'].domain + \
|
||||
reverse('registration_activate', args=[activation_key])
|
||||
return context
|
||||
|
||||
|
||||
@@ -299,20 +350,24 @@ class ActivationView(BaseActivationView):
|
||||
|
||||
|
||||
def registration_complete(request):
|
||||
messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.'))
|
||||
messages.success(request, _(
|
||||
'Sent confirmation email. Please follow the link to confirm your email address.'))
|
||||
return redirect('authentication:login')
|
||||
|
||||
|
||||
def activation_complete(request):
|
||||
messages.success(request, _('Confirmed your email address. Please login to continue.'))
|
||||
messages.success(request, _(
|
||||
'Confirmed your email address. Please login to continue.'))
|
||||
return redirect('authentication:dashboard')
|
||||
|
||||
|
||||
def registration_closed(request):
|
||||
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
|
||||
messages.error(request, _(
|
||||
'Registration of new accounts is not allowed at this time.'))
|
||||
return redirect('authentication:login')
|
||||
|
||||
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def task_counts(request) -> JsonResponse:
|
||||
"""Return task counts as JSON for an AJAX call."""
|
||||
data = {
|
||||
@@ -320,3 +375,31 @@ def task_counts(request) -> JsonResponse:
|
||||
"tasks_queued": queued_tasks_count()
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
def check_for_override_esi_error_message(response):
|
||||
if response.status_code in ESI_ERROR_MESSAGE_OVERRIDES:
|
||||
return {"error": ESI_ERROR_MESSAGE_OVERRIDES.get(response.status_code)}
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def esi_check(request) -> JsonResponse:
|
||||
"""Return if ESI ok With error messages and codes as JSON"""
|
||||
_r = requests.get("https://esi.evetech.net/latest/status/?datasource=tranquility")
|
||||
|
||||
data = {
|
||||
"status": _r.status_code,
|
||||
"data": check_for_override_esi_error_message(_r)
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard_bs3(request):
|
||||
"""Render dashboard view with BS3 theme.
|
||||
|
||||
This is an internal view used for testing BS3 backward compatibility in AA4 only.
|
||||
"""
|
||||
return render(request, 'authentication/dashboard_bs3.html')
|
||||
|
||||
@@ -12,13 +12,14 @@ class StartProject(BaseStartProject):
|
||||
parser.add_argument('--python', help='The path to the python executable.')
|
||||
parser.add_argument('--celery', help='The path to the celery executable.')
|
||||
parser.add_argument('--gunicorn', help='The path to the gunicorn executable.')
|
||||
parser.add_argument('--memmon', help='The path to the memmon executable.')
|
||||
|
||||
|
||||
def create_project(parser, options, args):
|
||||
# Validate args
|
||||
if len(args) < 2:
|
||||
parser.error("Please specify a name for your Alliance Auth installation.")
|
||||
elif len(args) > 3:
|
||||
elif len(args) > 4:
|
||||
parser.error("Too many arguments.")
|
||||
|
||||
# First find the path to Alliance Auth
|
||||
@@ -32,6 +33,7 @@ def create_project(parser, options, args):
|
||||
'python': shutil.which('python'),
|
||||
'gunicorn': shutil.which('gunicorn'),
|
||||
'celery': shutil.which('celery'),
|
||||
'memmon': shutil.which('memmon'),
|
||||
'extensions': ['py', 'conf', 'json'],
|
||||
}
|
||||
|
||||
|
||||
171
allianceauth/checks.py
Normal file
171
allianceauth/checks.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from typing import List
|
||||
from django import db
|
||||
from django.core.checks import CheckMessage, Error, register, Warning
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
from django.utils import timezone
|
||||
from packaging.version import InvalidVersion, Version as Pep440Version
|
||||
from celery import current_app
|
||||
from django.conf import settings
|
||||
from sqlite3.dbapi2 import sqlite_version_info
|
||||
|
||||
"""
|
||||
A = System Packages
|
||||
B = Configuration
|
||||
"""
|
||||
|
||||
|
||||
@register()
|
||||
def django_settings(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
if hasattr(settings, "SITE_URL"):
|
||||
if settings.SITE_URL[-1] == "/":
|
||||
errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005"))
|
||||
else:
|
||||
errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006"))
|
||||
|
||||
if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"):
|
||||
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
|
||||
errors.append(Warning("'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", hint="", id="allianceauth.checks.B007"))
|
||||
else:
|
||||
errors.append(Error("No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", hint="", id="allianceauth.checks.B008"))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
try:
|
||||
redis_version = Pep440Version(get_redis_client().info()['redis_version'])
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm Redis Version"))
|
||||
return errors
|
||||
|
||||
if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A001"))
|
||||
elif redis_version.major == 7 and redis_version.minor == 0:
|
||||
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A002"))
|
||||
elif redis_version.major == 6 and redis_version.minor == 2:
|
||||
errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A018"))
|
||||
elif redis_version.major in [6, 5]:
|
||||
errors.append(Error(f"Redis {redis_version.public} EOL", hint="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/", id="allianceauth.checks.A003"))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "mysql":
|
||||
try:
|
||||
mysql_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm MySQL Version"))
|
||||
return errors
|
||||
|
||||
# MySQL 8
|
||||
if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 3:
|
||||
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 2:
|
||||
errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A006"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 1:
|
||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007"))
|
||||
elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008"))
|
||||
elif mysql_version.major < 8: # This will also catch Mariadb 5.x
|
||||
errors.append(Error(f"MySQL or MariaDB {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A009"))
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB
|
||||
try:
|
||||
mariadb_version = Pep440Version(".".join(str(i) for i in connection.mysql_version))
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm MariaDB Version"))
|
||||
return errors
|
||||
|
||||
# MariaDB 11
|
||||
if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010"))
|
||||
elif mariadb_version.major == 11 and mariadb_version.minor == 2:
|
||||
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018"))
|
||||
if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011"))
|
||||
elif mariadb_version.major == 11 and mariadb_version.minor == 1:
|
||||
errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019"))
|
||||
if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012"))
|
||||
elif mariadb_version.major == 11 and mariadb_version.minor in [0, 3]: # Demote versions down here once EOL
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A013"))
|
||||
|
||||
# MariaDB 10
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014"))
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015"))
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=timezone.utc):
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016"))
|
||||
elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL
|
||||
errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017"))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "sqlite":
|
||||
try:
|
||||
sqlite_version = Pep440Version(".".join(str(i) for i in sqlite_version_info))
|
||||
except InvalidVersion:
|
||||
errors.append(Warning("Unable to confirm SQLite Version"))
|
||||
return errors
|
||||
if sqlite_version.major == 3 and sqlite_version.minor < 27:
|
||||
errors.append(Error(f"SQLite {sqlite_version.public} Unsupported by Django", hint="https://pkgs.org/download/sqlite3", id="allianceauth.checks.A020"))
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def sql_settings(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
for connection in db.connections.all():
|
||||
if connection.vendor == "mysql":
|
||||
if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4":
|
||||
errors.append(Error("SQL Charset is not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001"))
|
||||
# This hasn't actually been set on AA yet
|
||||
# if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4_unicode_ci":
|
||||
# errors.append(Error("SQL Collation is not set correctly", hint="", id="allianceauth.checks.B002"))
|
||||
# if connection.vendor == "sqlite":
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register()
|
||||
def celery_settings(app_configs, **kwargs) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}:
|
||||
errors.append(Error("Celery Priorities are not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003"))
|
||||
|
||||
if current_app.conf.broker_connection_retry_on_startup != True:
|
||||
errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004"))
|
||||
return errors
|
||||
|
||||
|
||||
# IDEAS
|
||||
|
||||
# Any other celery things weve manually changed over the years
|
||||
# I'd be happy to add Community App checks, old versions the owners dont want to support etc.
|
||||
|
||||
|
||||
# Check Default Collation on DB
|
||||
# Check Charset Collation on all tables
|
||||
@@ -1,4 +1,5 @@
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from allianceauth.menu.hooks import MenuItemHook
|
||||
from allianceauth.services.hooks import UrlHook
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from allianceauth import hooks
|
||||
from allianceauth.corputils import urls
|
||||
@@ -9,7 +10,7 @@ class CorpStats(MenuItemHook):
|
||||
MenuItemHook.__init__(
|
||||
self,
|
||||
_('Corporation Stats'),
|
||||
'fas fa-share-alt fa-fw',
|
||||
'fa-solid fa-share-nodes',
|
||||
'corputils:view',
|
||||
navactive=['corputils:']
|
||||
)
|
||||
|
||||
@@ -1,37 +1,63 @@
|
||||
{% extends 'allianceauth/base.html' %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block page_title %}{% translate "Corporation Member Data" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Corporation Member Data" %}</h1>
|
||||
<div class="col-lg-10 col-lg-offset-1 container">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="dropdown">
|
||||
<a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% translate "Corporations" %}<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
|
||||
{% for corpstat in available %}
|
||||
<li>
|
||||
<a href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">{{ corpstat.corp.corporation_name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% if perms.corputils.add_corpstats %}
|
||||
<li>
|
||||
<a href="{% url 'corputils:add' %}">{% translate "Add" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate "Search all corporations..." %}{% endif %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
{% block member_data %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block page_title %}
|
||||
{% translate "Corporation Member Data" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Corporation Member Data" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block header_nav_collapse_left %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
|
||||
{% translate "Corporations" %}
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
{% for corpstat in available %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">
|
||||
{{ corpstat.corp.corporation_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if perms.corputils.add_corpstats %}
|
||||
{% if available.count >= 1 %}
|
||||
<li> </li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'corputils:add' %}">
|
||||
{% translate "Add corporation" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block header_nav_collapse_right %}
|
||||
<li class="nav-item">
|
||||
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="search_string"
|
||||
placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate 'Search all corporations...' %}{% endif %}"
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
{% block member_data %}
|
||||
{% endblock member_data %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,219 +1,276 @@
|
||||
{% extends 'corputils/base.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block member_data %}
|
||||
{% if corpstats %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center col-lg-6{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}">
|
||||
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}">
|
||||
</td>
|
||||
{% if corpstats.corp.alliance %}
|
||||
<td class="text-center col-lg-6">
|
||||
<img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}" alt="{{ corpstats.corp.alliance.alliance_name }}">
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center"><h4>{{ corpstats.corp.corporation_name }}</h4></td>
|
||||
{% if corpstats.corp.alliance %}
|
||||
<td class="text-center"><h4>{{ corpstats.corp.alliance.alliance_name }}</h4></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table class="table text-center">
|
||||
<tr>
|
||||
<td>
|
||||
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}">
|
||||
</td>
|
||||
|
||||
{% if corpstats.corp.alliance %}
|
||||
<td>
|
||||
<img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}" alt="{{ corpstats.corp.alliance.alliance_name }}">
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><p class="h4">{{ corpstats.corp.corporation_name }}</p></td>
|
||||
|
||||
{% if corpstats.corp.alliance %}
|
||||
<td><p class="h4">{{ corpstats.corp.alliance.alliance_name }}</p></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<ul class="nav nav-pills pull-left">
|
||||
<li class="active"><a href="#mains" data-toggle="pill">{% translate 'Mains' %} ({{ total_mains }})</a></li>
|
||||
<li><a href="#members" data-toggle="pill">{% translate 'Members' %} ({{ corpstats.member_count }})</a></li>
|
||||
<li><a href="#unregistered" data-toggle="pill">{% translate 'Unregistered' %} ({{ unregistered.count }})</a></li>
|
||||
</ul>
|
||||
<div class="pull-right hidden-xs">
|
||||
{% translate "Last update:" %} {{ corpstats.last_update|naturaltime }}
|
||||
<a class="btn btn-success" type="button" href="{% url 'corputils:update' corpstats.corp.corporation_id %}" title="Update Now">
|
||||
<span class="glyphicon glyphicon-refresh"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="card card-default mt-4">
|
||||
<div class="card-header clearfix" role="tablist">
|
||||
<ul class="nav nav-pills float-start">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a
|
||||
class="nav-link active"
|
||||
id="mains"
|
||||
data-bs-toggle="tab"
|
||||
href="#tab-mains"
|
||||
role="tab"
|
||||
aria-controls="tab-mains"
|
||||
aria-selected="true"
|
||||
>
|
||||
{% translate 'Mains' %} ({{ total_mains }})
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" role="presentation">
|
||||
<a
|
||||
class="nav-link"
|
||||
id="members"
|
||||
data-bs-toggle="tab"
|
||||
href="#tab-members"
|
||||
role="tab"
|
||||
aria-controls="tab-members"
|
||||
aria-selected="false"
|
||||
>
|
||||
{% translate 'Members' %} ({{ corpstats.member_count }})
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
id="unregistered"
|
||||
data-bs-toggle="tab"
|
||||
href="#tab-unregistered"
|
||||
role="tab"
|
||||
aria-controls="tab-unregistered"
|
||||
aria-selected="false"
|
||||
>
|
||||
{% translate 'Unregistered' %} ({{ unregistered.count }})
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="float-end d-none d-sm-block">
|
||||
{% translate "Last update:" %} {{ corpstats.last_update|naturaltime }}
|
||||
|
||||
<a
|
||||
class="btn btn-success btn-sm ms-2"
|
||||
type="button"
|
||||
href="{% url 'corputils:update' corpstats.corp.corporation_id %}"
|
||||
title="{% translate 'Update Now' %}"
|
||||
>
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tab-mains" role="tabpanel" aria-labelledby="tab-mains">
|
||||
{% if mains %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="table-mains">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Main character" %}</th>
|
||||
<th>{% translate "Registered characters" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for id, main in mains.items %}
|
||||
<tr>
|
||||
<td class="text-center" style="vertical-align: middle;">
|
||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||
<img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
|
||||
<div class="caption">
|
||||
{{ main.main }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<table class="table table-hover">
|
||||
{% for alt in main.alts|dictsort:"character_name" %}
|
||||
{% if forloop.first %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Corporation" %}</th>
|
||||
<th>{% translate "Alliance" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td style="width: 5%;">
|
||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||
<img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}">
|
||||
</div>
|
||||
</td>
|
||||
<td style="width: 30%;">{{ alt.character_name }}</td>
|
||||
<td style="width: 30%;">{{ alt.corporation_name }}</td>
|
||||
<td style="width: 30%;">{{ alt.alliance_name|default_if_none:"" }}</td>
|
||||
<td style="width: 5%;">
|
||||
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge bg-danger" target="_blank">
|
||||
{% translate "Killboard" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade in active" id="mains">
|
||||
{% if mains %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="table-mains">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="height:1em;"><!-- Must have text or height to prevent clipping --></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for id, main in mains.items %}
|
||||
<tr>
|
||||
<td class="text-center" style="vertical-align:middle">
|
||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||
<img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
|
||||
<div class="caption text-center">
|
||||
{{ main.main }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<table class="table table-hover">
|
||||
{% for alt in main.alts %}
|
||||
{% if forloop.first %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% translate "Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Alliance" %}</th>
|
||||
<th class="text-center"></th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="text-center" style="width:5%">
|
||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||
<img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}">
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
|
||||
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
|
||||
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
|
||||
<td class="text-center" style="width:5%">
|
||||
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="label label-danger" target="_blank">
|
||||
{% translate "Killboard" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade" id="tab-members" role="tabpanel" aria-labelledby="tab-members">
|
||||
{% if members %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="table-members">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th></th>
|
||||
<th>{% translate "Main Character" %}</th>
|
||||
<th>{% translate "Main Corporation" %}</th>
|
||||
<th>{% translate "Main Alliance" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
||||
<td>{{ member }}</td>
|
||||
<td>
|
||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||
</td>
|
||||
<td>{{ member.character_ownership.user.profile.main_character.character_name }}</td>
|
||||
<td>{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
|
||||
<td>{{ member.character_ownership.user.profile.main_character.alliance_name|default_if_none:"" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% for member in unregistered %}
|
||||
<tr class="table-danger">
|
||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||
<td>{{ member.character_name }}</td>
|
||||
<td>
|
||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="members">
|
||||
{% if members %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="table-members">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% translate "Character" %}</th>
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Main Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Main Alliance" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
||||
<td class="text-center">{{ member }}</td>
|
||||
<td class="text-center">
|
||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||
</td>
|
||||
<td class="text-center">{{ member.character_ownership.user.profile.main_character.character_name }}</td>
|
||||
<td class="text-center">{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
|
||||
<td class="text-center">{{ member.character_ownership.user.profile.main_character.alliance_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for member in unregistered %}
|
||||
<tr class="danger">
|
||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||
<td class="text-center">{{ member.character_name }}</td>
|
||||
<td class="text-center">
|
||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||
</td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-unregistered" role="tabpanel" aria-labelledby="tab-unregistered">
|
||||
{% if unregistered %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="table-unregistered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for member in unregistered %}
|
||||
<tr class="table-danger">
|
||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||
<td>{{ member.character_name }}</td>
|
||||
<td>
|
||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">
|
||||
{% translate "Killboard" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="unregistered">
|
||||
{% if unregistered %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="table-unregistered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% translate "Character" %}</th>
|
||||
<th class="text-center"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in unregistered %}
|
||||
<tr class="danger">
|
||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||
<td class="text-center">{{ member.character_name }}</td>
|
||||
<td class="text-center">
|
||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">
|
||||
{% translate "Killboard" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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(){
|
||||
$('#table-mains').DataTable({
|
||||
"columnDefs": [
|
||||
{ "sortable": false, "targets": [1] },
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
$('#table-members').DataTable({
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
$('#table-unregistered').DataTable({
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
|
||||
});
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#table-mains').DataTable({
|
||||
"columnDefs": [
|
||||
{ "sortable": false, "targets": [1] },
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
|
||||
$('#table-members').DataTable({
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
|
||||
$('#table-unregistered').DataTable({
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
{% extends "corputils/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block member_data %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading clearfix">
|
||||
<div class="panel-title pull-left">{% translate "Search Results" %}</div>
|
||||
<div class="card card-default">
|
||||
<div class="card-header clearfix">
|
||||
<div class="card-title">{% translate "Search Results" %}</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="card-body mt-2">
|
||||
<table class="table table-hover" id="table-search">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% translate "Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "zKillboard" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Main Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Main Alliance" %}</th>
|
||||
<th></th>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Corporation" %}</th>
|
||||
<th>{% translate "zKillboard" %}</th>
|
||||
<th>{% translate "Main Character" %}</th>
|
||||
<th>{% translate "Main Corporation" %}</th>
|
||||
<th>{% translate "Main Alliance" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for result in results %}
|
||||
<tr {% if not result.1.registered %}class="danger"{% endif %}>
|
||||
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
||||
<td class="text-center">{{ result.1.character_name }}</td>
|
||||
<td class="text-center">{{ result.0.corp.corporation_name }}</td>
|
||||
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
||||
<td class="text-center">{{ result.1.main_character.character_name }}</td>
|
||||
<td class="text-center">{{ result.1.main_character.corporation_name }}</td>
|
||||
<td class="text-center">{{ result.1.main_character.alliance_name }}</td>
|
||||
<td><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
||||
<td>{{ result.1.character_name }}</td>
|
||||
<td >{{ result.0.corp.corporation_name }}</td>
|
||||
<td><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
||||
<td>{{ result.1.main_character.character_name }}</td>
|
||||
<td>{{ result.1.main_character.corporation_name }}</td>
|
||||
<td>{{ result.1.main_character.alliance_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -35,17 +38,20 @@
|
||||
</div>
|
||||
</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(){
|
||||
$('#table-search').DataTable({
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#table-search').DataTable({
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -93,7 +93,7 @@ class AutogroupsConfigTestCase(TestCase):
|
||||
group_qs = Group.objects.filter(pk=group.pk)
|
||||
|
||||
self.assertIn(group, self.member.groups.all())
|
||||
self.assertQuerysetEqual(self.member.groups.all(), map(repr, pre_groups | group_qs), ordered=False)
|
||||
self.assertQuerySetEqual(self.member.groups.all(), pre_groups | group_qs, ordered=False)
|
||||
|
||||
def test_update_alliance_group_membership_no_main_character(self):
|
||||
obj = AutogroupsConfig.objects.create()
|
||||
@@ -172,7 +172,7 @@ class AutogroupsConfigTestCase(TestCase):
|
||||
group_qs = Group.objects.filter(pk=group.pk)
|
||||
|
||||
self.assertIn(group, self.member.groups.all())
|
||||
self.assertQuerysetEqual(self.member.groups.all(), map(repr, pre_groups | group_qs), ordered=False)
|
||||
self.assertQuerySetEqual(self.member.groups.all(), pre_groups | group_qs, ordered=False)
|
||||
|
||||
def test_update_corp_group_membership_no_state(self):
|
||||
obj = AutogroupsConfig.objects.create(corp_groups=True)
|
||||
|
||||
@@ -8,77 +8,70 @@ Needs to be called with a context containing three objects:
|
||||
|
||||
-->
|
||||
|
||||
{% extends 'allianceauth/base.html' %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
{% load evelinks %}
|
||||
|
||||
{% block page_title %}Evelinks examples{% endblock %}
|
||||
{% block page_title %}Evelinks Examples{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
{% include "framework/header/page-header.html" with title="Evelinks templatetags examples" %}
|
||||
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">Evelinks templatetags examples</h1>
|
||||
<div class="col-lg-12 container">
|
||||
|
||||
<h2>profile URLs</h2>
|
||||
|
||||
<div class="rows">
|
||||
|
||||
<div class="col-md-4">
|
||||
<h3>evewho</h3>
|
||||
<p><a href="{{ my_character|evewho_character_url}}">character from character object</a></p>
|
||||
<p><a href="{{ my_corporation|evewho_corporation_url}}">corporation form corporation object</a></p>
|
||||
<p><a href="{{ my_character|evewho_corporation_url}}">corporation from charachter object</a></p>
|
||||
<p><a href="{{ my_alliance|evewho_alliance_url}}">alliance from alliance object</a></p>
|
||||
<p><a href="{{ my_character|evewho_alliance_url}}">alliance from character object</a></p>
|
||||
<p><a href="{{ my_character|evewho_character_url }}">character from character object</a></p>
|
||||
<p><a href="{{ my_corporation|evewho_corporation_url }}">corporation form corporation object</a></p>
|
||||
<p><a href="{{ my_character|evewho_corporation_url }}">corporation from charachter object</a></p>
|
||||
<p><a href="{{ my_alliance|evewho_alliance_url }}">alliance from alliance object</a></p>
|
||||
<p><a href="{{ my_character|evewho_alliance_url }}">alliance from character object</a></p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h3>dotlan</h3>
|
||||
<p><a href="{{ my_character|dotlan_corporation_url}}">corporation form character object</a></p>
|
||||
<p><a href="{{ my_corporation|dotlan_corporation_url}}">corporation form corporation object</a></p>
|
||||
<p><a href="{{ my_character|dotlan_alliance_url}}">alliance from character object</a></p>
|
||||
<p><a href="{{ my_alliance|dotlan_alliance_url}}">alliance from alliance object</a></p>
|
||||
<p><a href="{{ 'Black Rise'|dotlan_region_url}}">region from name string</a></p>
|
||||
<p><a href="{{ 'Tama'|dotlan_solar_system_url}}">solar system from name string</a></p>
|
||||
<p><a href="{{ my_character|dotlan_corporation_url }}">corporation form character object</a></p>
|
||||
<p><a href="{{ my_corporation|dotlan_corporation_url }}">corporation form corporation object</a></p>
|
||||
<p><a href="{{ my_character|dotlan_alliance_url }}">alliance from character object</a></p>
|
||||
<p><a href="{{ my_alliance|dotlan_alliance_url }}">alliance from alliance object</a></p>
|
||||
<p><a href="{{ 'Black Rise'|dotlan_region_url }}">region from name string</a></p>
|
||||
<p><a href="{{ 'Tama'|dotlan_solar_system_url }}">solar system from name string</a></p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h3>zkillboard</h3>
|
||||
<p><a href="{{ my_character|zkillboard_character_url}}">character from character object</a></p>
|
||||
<p><a href="{{ my_character|zkillboard_corporation_url}}">corporation from character object</a></p>
|
||||
<p><a href="{{ my_corporation|zkillboard_corporation_url}}">corporation form corporation object</a></p>
|
||||
<p><a href="{{ my_character|zkillboard_alliance_url}}">alliance from character object</a></p>
|
||||
<p><a href="{{ my_alliance|zkillboard_alliance_url}}">alliance from alliance object</a></p>
|
||||
<p><a href="{{ 10000069|zkillboard_region_url}}">region from ID</a></p>
|
||||
<p><a href="{{ 30002813|zkillboard_solar_system_url}}">solar sytem from ID</a></p>
|
||||
<p><a href="{{ my_character|zkillboard_character_url }}">character from character object</a></p>
|
||||
<p><a href="{{ my_character|zkillboard_corporation_url }}">corporation from character object</a></p>
|
||||
<p><a href="{{ my_corporation|zkillboard_corporation_url }}">corporation form corporation object</a></p>
|
||||
<p><a href="{{ my_character|zkillboard_alliance_url }}">alliance from character object</a></p>
|
||||
<p><a href="{{ my_alliance|zkillboard_alliance_url }}">alliance from alliance object</a></p>
|
||||
<p><a href="{{ 10000069|zkillboard_region_url }}">region from ID</a></p>
|
||||
<p><a href="{{ 30002813|zkillboard_solar_system_url }}">solar sytem from ID</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>image URLs</h2>
|
||||
|
||||
<div class="rows">
|
||||
|
||||
<div class="col-md-4">
|
||||
<p>character from ID: <img src="{{ my_character.character_id|character_portrait_url:128}}"></p>
|
||||
<p>character from character object: <img src="{{ my_character|character_portrait_url:128}}"></p>
|
||||
<p>character from ID: <img src="{{ my_character.character_id|character_portrait_url:128 }}"></p>
|
||||
<p>character from character object: <img src="{{ my_character|character_portrait_url:128 }}"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<p>corporation from ID: <img src="{{ my_character.corporation_id|corporation_logo_url:128}}"></p>
|
||||
<p>corporation from character object: <img src="{{ my_character|corporation_logo_url:128}}"></p>
|
||||
<p>corporation from corporation object: <img src="{{ my_corporation|corporation_logo_url:128}}"></p>
|
||||
<p>corporation from ID: <img src="{{ my_character.corporation_id|corporation_logo_url:128 }}"></p>
|
||||
<p>corporation from character object: <img src="{{ my_character|corporation_logo_url:128 }}"></p>
|
||||
<p>corporation from corporation object: <img src="{{ my_corporation|corporation_logo_url:128 }}"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<p>alliance from ID: <img src="{{ my_character.alliance_id|alliance_logo_url:128}}"></p>
|
||||
<p>alliance from character object: <img src="{{ my_character|alliance_logo_url:128}}"></p>
|
||||
<p>alliance from alliance object: <img src="{{ my_alliance|alliance_logo_url:128}}"></p>
|
||||
<p>alliance from ID: <img src="{{ my_character.alliance_id|alliance_logo_url:128 }}"></p>
|
||||
<p>alliance from character object: <img src="{{ my_character|alliance_logo_url:128 }}"></p>
|
||||
<p>alliance from alliance object: <img src="{{ my_alliance|alliance_logo_url:128 }}"></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from allianceauth.menu.hooks import MenuItemHook
|
||||
from . import urls
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from allianceauth import hooks
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from allianceauth.services.hooks import UrlHook
|
||||
|
||||
|
||||
@hooks.register('menu_item_hook')
|
||||
def register_menu():
|
||||
return MenuItemHook(_('Fleet Activity Tracking'), 'fas fa-users fa-fw', 'fatlink:view',
|
||||
return MenuItemHook(_('Fleet Activity Tracking'), 'fa-solid fa-users', 'fatlink:view',
|
||||
navactive=['fatlink:'])
|
||||
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class FatlinkForm(forms.Form):
|
||||
fleet = forms.CharField(label=_("Fleet Name"), max_length=50)
|
||||
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, max_value=2147483647, help_text=_('minutes'))
|
||||
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, max_value=2147483647, help_text=_('Duration of the fat-link in minutes'))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
from django.utils.timezone import utc
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -15,6 +14,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='fatlink',
|
||||
name='fatdatetime',
|
||||
field=models.DateTimeField(default=datetime.datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=utc)),
|
||||
field=models.DateTimeField(default=datetime.datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=datetime.timezone.utc)),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Migration to AA Framework API method
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import allianceauth.framework.api.user
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("fleetactivitytracking", "0006_auto_20180803_0430"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="fatlink",
|
||||
name="creator",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.SET(allianceauth.framework.api.user.get_sentinel_user),
|
||||
to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -3,10 +3,7 @@ from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
|
||||
|
||||
def get_sentinel_user():
|
||||
return User.objects.get_or_create(username='deleted')[0]
|
||||
from allianceauth.framework.api.user import get_sentinel_user
|
||||
|
||||
|
||||
class Fatlink(models.Model):
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
{% extends 'allianceauth/base.html' %}
|
||||
{% extends 'allianceauth/base-bs5.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block page_title %}{% translate "Fleet Participation" %}{% endblock %}
|
||||
|
||||
{% block page_title %}
|
||||
{% translate "Fleet Participation" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Character not found!" %}</h1>
|
||||
<div class="col-lg-12 container" id="example">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">{{ character_name }}</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-lg-2 col-sm-2">
|
||||
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}" alt="{{ character_name }}">
|
||||
<div>
|
||||
{% translate "Character not found!" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
<div class="col-lg-12 container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
{{ character_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-10 col-sm-2">
|
||||
<div class="alert alert-danger" role="alert">{% translate "Character not registered!" %}</div>
|
||||
{% translate "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% translate "Add it here" %}</a> {% translate "before attempting to click fleet attendance links." %}
|
||||
|
||||
<div class="card-body">
|
||||
<div class="col-lg-2 col-sm-2">
|
||||
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}" alt="{{ character_name }}">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-10 col-sm-2">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% translate "Character not registered!" %}
|
||||
</div>
|
||||
|
||||
{% translate "This character is not associated with an auth account." %}
|
||||
<a href="{% url 'authentication:add_character' %}">{% translate "Add it here" %}</a>
|
||||
{% translate "before attempting to click fleet attendance links." %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
{% translate "Create Fatlink" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
{% translate "Create Fatlink" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
<div>
|
||||
{% if badrequest %}
|
||||
<div class="alert alert-danger" role="alert">{% translate "Bad request!" %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% for message in errormessages %}
|
||||
<div class="alert alert-danger" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="card card-primary border-0">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
{% translate "Fatlink details" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<form role="form" action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
<div class="form-group mt-3 clearfix">
|
||||
{% translate "Create fatlink" as button_text %}
|
||||
{% bootstrap_button button_class="btn btn-primary" content=button_text name="submit_fat" id="submit_fat" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,31 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Create Fatlink" %}{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Create Fleet Operation" %}</h1>
|
||||
|
||||
<div class="container-fluid">
|
||||
{% if badrequest %}
|
||||
<div class="alert alert-danger" role="alert">{% translate "Bad request!" %}</div>
|
||||
{% endif %}
|
||||
{% for message in errormessages %}
|
||||
<div class="alert alert-danger" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="row">
|
||||
<form class="form-signin" role="form" action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form|bootstrap }}
|
||||
<br>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" name="submit_fat">{% translate "Create fatlink" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,21 +1,35 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
|
||||
|
||||
{% block page_title %}
|
||||
{% translate "Fatlink view" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Edit fatlink" %} "{{ fatlink }}"
|
||||
<div class="text-right">
|
||||
<form>
|
||||
<button type="submit" onclick="return confirm('Are you sure?')" class="btn btn-danger" name="deletefat" value="True">
|
||||
{% translate "Delete fat" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{% translate "Edit fatlink" %} "{{ fatlink }}"
|
||||
</h1>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">{% translate "Registered characters" %}</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<form>
|
||||
<button type="submit" onclick="return confirm('{% translate "Are you sure?" %}')" class="btn btn-danger" name="deletefat" value="True">
|
||||
{% translate "Delete fat" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{% translate "Registered characters" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table table-responsive table-hover">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "User" %}</th>
|
||||
@@ -25,21 +39,23 @@
|
||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
{% for fat in registered_fats %}
|
||||
<tr>
|
||||
<td class="text-center">{{ fat.user }}</td>
|
||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||
{% if fat.station != "No Station" %}
|
||||
<td class="text-center">{% blocktranslate %}Docked in {% endblocktranslate %}{{ fat.system }}</td>
|
||||
{% else %}
|
||||
<td class="text-center">{{ fat.system }}</td>
|
||||
{% endif %}
|
||||
<td class="text-center">
|
||||
{% if fat.station != "No Station" %}
|
||||
{% translate "Docked in" %}
|
||||
{% endif %}
|
||||
{{ fat.system }}
|
||||
</td>
|
||||
<td class="text-center">{{ fat.shiptype }}</td>
|
||||
<td class="text-center">{{ fat.fatlink.fatdatetime }}</td>
|
||||
<td class="text-center">
|
||||
<form>
|
||||
<button type="submit" class="btn btn-warning" name="removechar" value="{{ fat.character.character_id }}">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
<i class="fa-solid fa-trash-can fa-fw"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
@@ -1,71 +1,104 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Personal fatlink statistics" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
{% if char_id %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% translate "Next month" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
</h1>
|
||||
<h2>
|
||||
{% blocktranslate count links=n_fats trimmed %}
|
||||
{{ user }} has collected one link this month.
|
||||
{% plural %}
|
||||
{{ user }} has collected {{ links }} links this month.
|
||||
{% endblocktranslate %}
|
||||
</h2>
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="col-md-2 text-center">{% translate "Ship" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Times used" %}</th>
|
||||
</tr>
|
||||
{% for ship, n_fats in shipStats %}
|
||||
<tr>
|
||||
<td class="text-center">{{ ship }}</td>
|
||||
<td class="text-center">{{ n_fats }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if created_fats %}
|
||||
<h2>
|
||||
{% blocktranslate count links=n_created_fats trimmed %}
|
||||
{{ user }} has created one link this month.
|
||||
{% plural %}
|
||||
{{ user }} has created {{ links }} links this month.
|
||||
{% endblocktranslate %}
|
||||
</h2>
|
||||
{% if created_fats %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Fleet" %}</th>
|
||||
<th class="text-center">{% translate "Creator" %}</th>
|
||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||
<th class="text-center">{% translate "Duration" %}</th>
|
||||
<th class="text-center">{% translate "Edit" %}</th>
|
||||
</tr>
|
||||
{% for link in created_fats %}
|
||||
<tr>
|
||||
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
|
||||
<td class="text-center">{{ link.creator.username }}</td>
|
||||
<td class="text-center">{{ link.fatdatetime }}</td>
|
||||
<td class="text-center">{{ link.duration }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:modify' link.hash %}">
|
||||
<button type="button" class="btn btn-info"><span
|
||||
class="glyphicon glyphicon-edit"></span></button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% if char_id %}
|
||||
<div class="text-end mb-3">
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">
|
||||
{% translate "Previous month" %}
|
||||
</a>
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">
|
||||
{% translate "Next month" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
{% blocktranslate count links=n_fats trimmed %}
|
||||
{{ user }} has collected one link this month.
|
||||
{% plural %}
|
||||
{{ user }} has collected {{ links }} links this month.
|
||||
{% endblocktranslate %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="col-md-2 text-center">{% translate "Ship" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Times used" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for ship, n_fats in shipStats %}
|
||||
<tr>
|
||||
<td class="text-center">{{ ship }}</td>
|
||||
<td class="text-center">{{ n_fats }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if created_fats %}
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
{% blocktranslate count links=n_created_fats trimmed %}
|
||||
{{ user }} has created one link this month.
|
||||
{% plural %}
|
||||
{{ user }} has created {{ links }} links this month.
|
||||
{% endblocktranslate %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Fleet" %}</th>
|
||||
<th class="text-center">{% translate "Creator" %}</th>
|
||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||
<th class="text-center">{% translate "Duration" %}</th>
|
||||
<th class="text-center">{% translate "Edit" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for link in created_fats %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">
|
||||
{{ link.fleet }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">{{ link.creator.username }}</td>
|
||||
<td class="text-center">{{ link.fatdatetime }}</td>
|
||||
<td class="text-center">{{ link.duration }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:modify' link.hash %}">
|
||||
<button type="button" class="btn btn-info">
|
||||
<i class="fa-solid fa-pen-to-square fa-fw"></i>
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Personal fatlink statistics" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ year }}{% endblocktranslate %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:personal_statistics_year' previous_year %}" class="btn btn-info">{% translate "Previous year" %}</a>
|
||||
{% if next_year %}
|
||||
<a href="{% url 'fatlink:personal_statistics_year' next_year %}" class="btn btn-info">{% translate "Next year" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{% blocktranslate %}Participation data statistics for {{ year }}{% endblocktranslate %}
|
||||
</h1>
|
||||
<div class="col-lg-2 col-lg-offset-5">
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="col-md-2 text-center">{% translate "Month" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Fats" %}</th>
|
||||
</tr>
|
||||
{% for monthnr, month, n_fats in monthlystats %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:personal_statistics_month' year monthnr %}">
|
||||
{{ month }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">{{ n_fats }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<a href="{% url "fatlink:personal_statistics_year" previous_year %}" class="btn btn-info"><i class="fa-solid fa-chevron-left"></i> {% translate "Previous year" %}</a>
|
||||
|
||||
{% if next_year %}
|
||||
<a href="{% url "fatlink:personal_statistics_year" next_year %}" class="btn btn-info">{% translate "Next year" %} <i class="fa-solid fa-chevron-right"></i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 offset-lg-5">
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Month" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for monthnr, month, n_fats in monthlystats %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:personal_statistics_month' year monthnr %}">{{ month }}</a>
|
||||
</td>
|
||||
<td class="text-center">{{ n_fats }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Fatlink Corp Statistics" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Fatlink Corp Statistics" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:statistics_corp_month' corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
{% if next_month %}
|
||||
<a href="{% url 'fatlink:statistics_corp_month' corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
</h1>
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<a href="{% url "fatlink:statistics_corp_month" corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
|
||||
{% if next_month %}
|
||||
<a href="{% url "fatlink:statistics_corp_month" corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if fatStats %}
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="col-md-1"></th>
|
||||
<th class="col-md-2 text-center">{% translate "Main Character" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Characters" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Fats" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Average fats" %}
|
||||
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i>
|
||||
</th>
|
||||
</tr>
|
||||
{% for memberStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}">
|
||||
</td>
|
||||
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
|
||||
<td class="text-center">{{ memberStat.n_chars }}</td>
|
||||
<td class="text-center">{{ memberStat.n_fats }}</td>
|
||||
<td class="text-center">{{ memberStat.avg_fat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th scope="col" class="col-md-1"></th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Main Character" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Characters" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">
|
||||
{% translate "Average fats" %}
|
||||
<i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
|
||||
</th>
|
||||
</tr>
|
||||
{% for memberStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}">
|
||||
</td>
|
||||
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
|
||||
<td class="text-center">{{ memberStat.n_chars }}</td>
|
||||
<td class="text-center">{{ memberStat.n_fats }}</td>
|
||||
<td class="text-center">{{ memberStat.avg_fat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block extra_script %}
|
||||
$(document).ready(function () {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
});
|
||||
{% endblock extra_script %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
});
|
||||
</script>
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
@@ -1,48 +1,67 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Fatlink statistics" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Fatlink Statistics" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:statistics_month' previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
{% if next_month %}
|
||||
<a href="{% url 'fatlink:statistics_month' next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
|
||||
</h1>
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<a href="{% url "fatlink:statistics_month" previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
|
||||
|
||||
{% if next_month %}
|
||||
<a href="{% url 'fatlink:statistics_month' next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if fatStats %}
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="col-md-1"></th>
|
||||
<th class="col-md-2 text-center">{% translate "Ticker" %}</th>
|
||||
<th class="col-md-5 text-center">{% translate "Corp" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Members" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Fats" %}</th>
|
||||
<th class="col-md-2 text-center">{% translate "Average fats" %}
|
||||
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i>
|
||||
</th>
|
||||
</tr>
|
||||
{% for corpStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}">
|
||||
</td>
|
||||
<td class="text-center"><a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a></td>
|
||||
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
|
||||
<td class="text-center">{{ corpStat.corp.member_count }}</td>
|
||||
<td class="text-center">{{ corpStat.n_fats }}</td>
|
||||
<td class="text-center">{{ corpStat.avg_fat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th scope="col" class="col-md-1"></th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Ticker" %}</th>
|
||||
<th scope="col" class="col-md-5 text-center">{% translate "Corp" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Members" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
|
||||
<th scope="col" class="col-md-2 text-center">
|
||||
{% translate "Average fats" %}
|
||||
<i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
|
||||
</th>
|
||||
</tr>
|
||||
{% for corpStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a>
|
||||
</td>
|
||||
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
|
||||
<td class="text-center">{{ corpStat.corp.member_count }}</td>
|
||||
<td class="text-center">{{ corpStat.n_fats }}</td>
|
||||
<td class="text-center">{{ corpStat.avg_fat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block extra_script %}
|
||||
$(document).ready(function () {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
});
|
||||
{% endblock extra_script %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$("[rel=tooltip]").tooltip();
|
||||
});
|
||||
</script>
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
@@ -1,99 +1,124 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Fatlink view" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Fleet Activity Tracking" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Participation data" %}</h1>
|
||||
<table class="table">
|
||||
<div>
|
||||
{% translate "Participation data" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th class="col-md-11">
|
||||
<h4><b>{% translate "Most recent clicked fatlinks" %}</b>
|
||||
<th class="col-md-10">
|
||||
<h4>
|
||||
<b>{% translate "Most recent clicked fatlinks" %}</b>
|
||||
</h4>
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
<th class="col-md-2 align-self-end">
|
||||
<a href="{% url 'fatlink:personal_statistics' %}" class="btn btn-info">
|
||||
<i class="fa-solid fa-circle-info fa-fw"></i>
|
||||
{% translate "Personal statistics" %}
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
{% if fats %}
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Fleet" %}</th>
|
||||
<th class="text-center">{% translate "Character" %}</th>
|
||||
<th class="text-center">{% translate "System" %}</th>
|
||||
<th class="text-center">{% translate "Ship" %}</th>
|
||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||
</tr>
|
||||
{% for fat in fats %}
|
||||
<tr>
|
||||
<td class="text-center">{{ fat.fatlink.fleet }}</td>
|
||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||
{% if fat.station != "No Station" %}
|
||||
<td class="text-center">{% blocktranslate %}Docked in {% endblocktranslate %}{{ fat.system }}</td>
|
||||
{% else %}
|
||||
<td class="text-center">{{ fat.system }}</td>
|
||||
{% endif %}
|
||||
<td class="text-center">{{ fat.shiptype }}</td>
|
||||
<td class="text-center">{{ fat.fatlink.fatdatetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No fleet activity on record." %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.auth.fleetactivitytracking%}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="col-md-10">
|
||||
<h4><b>{% translate "Most recent fatlinks" %}</b>
|
||||
</h4>
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
<a href="{% url 'fatlink:statistics' %}" class="btn btn-info">
|
||||
{% translate "View statistics" %}
|
||||
</a>
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
<a href="{% url 'fatlink:create' %}" class="btn btn-success">
|
||||
{% translate "Create fatlink" %}
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
{% if fatlinks %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Name" %}</th>
|
||||
<th class="text-center">{% translate "Creator" %}</th>
|
||||
<th class="text-center">{% translate "Fleet" %}</th>
|
||||
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||
<th class="text-center">{% translate "Duration" %}</th>
|
||||
<th class="text-center">{% translate "Edit" %}</th>
|
||||
</tr>
|
||||
{% for link in fatlinks %}
|
||||
<tr>
|
||||
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
|
||||
<td class="text-center">{{ link.creator.username }}</td>
|
||||
<td class="text-center">{{ link.fleet }}</td>
|
||||
<td class="text-center">{{ link.fatdatetime }}</td>
|
||||
<td class="text-center">{{ link.duration }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info">
|
||||
<span class="glyphicon glyphicon-edit"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</table>
|
||||
{% if fats %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Character" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "System" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Ship" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for fat in fats %}
|
||||
<tr>
|
||||
<td class="text-center">{{ fat.fatlink.fleet }}</td>
|
||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||
{% if fat.station != "No Station" %}
|
||||
<td class="text-center">{% translate "Docked in" %} {{ fat.system }}</td>
|
||||
{% else %}
|
||||
<td class="text-center">{{ fat.system }}</td>
|
||||
{% endif %}
|
||||
<td class="text-center">{{ fat.shiptype }}</td>
|
||||
<td class="text-center">{{ fat.fatlink.fatdatetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No created fatlinks on record." %}</div>
|
||||
<div class="alert alert-warning text-center">{% translate "No fleet activity on record." %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.auth.fleetactivitytracking %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th class="col-md-8">
|
||||
<h4>
|
||||
<b>{% translate "Most recent fatlinks" %}</b>
|
||||
</h4>
|
||||
</th>
|
||||
<th class="col-md-2 align-self-end">
|
||||
<a href="{% url 'fatlink:statistics' %}" class="btn btn-info"><i class="fa-solid fa-eye fa-fw"></i> {% translate "View statistics" %}</a>
|
||||
</th>
|
||||
<th class="col-md-2 align-self-end">
|
||||
<a href="{% url 'fatlink:create' %}" class="btn btn-success"><i class="fa-solid fa-plus fa-fw"></i> {% translate "Create fatlink" %}</a>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if fatlinks %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th scope="col" class="text-center">{% translate "Name" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Creator" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Duration" %}</th>
|
||||
<th scope="col" class="text-center">{% translate "Edit" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for link in fatlinks %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a>
|
||||
</td>
|
||||
<td class="text-center">{{ link.creator.username }}</td>
|
||||
<td class="text-center">{{ link.fleet }}</td>
|
||||
<td class="text-center">{{ link.fatdatetime }}</td>
|
||||
<td class="text-center">
|
||||
{{ link.duration }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info">
|
||||
<i class="fa-solid fa-pen-to-square fa-fw"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No created fatlinks on record." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -352,11 +352,11 @@ def create_fatlink_view(request):
|
||||
for errorname, message in e.message_dict.items():
|
||||
messages.append(message[0].decode())
|
||||
context = {'form': form, 'errormessages': messages}
|
||||
return render(request, 'fleetactivitytracking/fatlinkformatter.html', context=context)
|
||||
return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context)
|
||||
else:
|
||||
form = FatlinkForm()
|
||||
context = {'form': form, 'badrequest': True}
|
||||
return render(request, 'fleetactivitytracking/fatlinkformatter.html', context=context)
|
||||
return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context)
|
||||
return redirect('fatlink:view')
|
||||
|
||||
else:
|
||||
@@ -365,7 +365,7 @@ def create_fatlink_view(request):
|
||||
|
||||
context = {'form': form}
|
||||
|
||||
return render(request, 'fleetactivitytracking/fatlinkformatter.html', context=context)
|
||||
return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -377,12 +377,12 @@ def modify_fatlink_view(request, fat_hash=None):
|
||||
if request.GET.get('removechar', None):
|
||||
character_id = request.GET.get('removechar')
|
||||
character = EveCharacter.objects.get(character_id=character_id)
|
||||
logger.debug(f"Removing character {character.character_name} from fleetactivitytracking {fatlink}")
|
||||
logger.debug(f"Removing character {character.character_name} from fleetactivitytracking {fatlink}")
|
||||
|
||||
Fat.objects.filter(fatlink=fatlink).filter(character=character).delete()
|
||||
|
||||
if request.GET.get('deletefat', None):
|
||||
logger.debug("Removing fleetactivitytracking %s" % fatlink)
|
||||
logger.debug("Removing fleetactivitytracking %s" % fatlink)
|
||||
fatlink.delete()
|
||||
return redirect('fatlink:view')
|
||||
|
||||
|
||||
3
allianceauth/framework/__init__.py
Normal file
3
allianceauth/framework/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Alliance Auth Framework
|
||||
"""
|
||||
57
allianceauth/framework/api/evecharacter.py
Normal file
57
allianceauth/framework/api/evecharacter.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Alliance Auth Evecharacter API
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allianceauth.authentication.models import CharacterOwnership
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.framework.api.user import get_sentinel_user
|
||||
|
||||
|
||||
def get_main_character_from_evecharacter(
|
||||
character: EveCharacter,
|
||||
) -> Optional[EveCharacter]:
|
||||
"""
|
||||
Get the main character for a given EveCharacter or None when no main character is set
|
||||
|
||||
:param character:
|
||||
:type character:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
try:
|
||||
userprofile = character.character_ownership.user.profile
|
||||
except (
|
||||
AttributeError,
|
||||
EveCharacter.character_ownership.RelatedObjectDoesNotExist,
|
||||
CharacterOwnership.user.RelatedObjectDoesNotExist,
|
||||
):
|
||||
return None
|
||||
|
||||
return userprofile.main_character
|
||||
|
||||
|
||||
def get_user_from_evecharacter(character: EveCharacter) -> User:
|
||||
"""
|
||||
Get the user for an EveCharacter or the sentinel user when no user is found
|
||||
|
||||
:param character:
|
||||
:type character:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
try:
|
||||
userprofile = character.character_ownership.user.profile
|
||||
except (
|
||||
AttributeError,
|
||||
EveCharacter.character_ownership.RelatedObjectDoesNotExist,
|
||||
CharacterOwnership.user.RelatedObjectDoesNotExist,
|
||||
):
|
||||
return get_sentinel_user()
|
||||
|
||||
return userprofile.user
|
||||
90
allianceauth/framework/api/user.py
Normal file
90
allianceauth/framework/api/user.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Alliance Auth User API
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allianceauth.authentication.models import CharacterOwnership
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
|
||||
|
||||
def get_all_characters_from_user(user: User) -> list:
|
||||
"""
|
||||
Get all characters from a user or an empty list
|
||||
when no characters are found for the user or the user is None
|
||||
|
||||
:param user:
|
||||
:type user:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
if user is None:
|
||||
return []
|
||||
|
||||
try:
|
||||
characters = [
|
||||
char.character for char in CharacterOwnership.objects.filter(user=user)
|
||||
]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
return characters
|
||||
|
||||
|
||||
def get_main_character_from_user(user: User) -> Optional[EveCharacter]:
|
||||
"""
|
||||
Get the main character from a user
|
||||
|
||||
:param user:
|
||||
:type user:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
main_character = user.profile.main_character
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
return main_character
|
||||
|
||||
|
||||
def get_main_character_name_from_user(user: User) -> str:
|
||||
"""
|
||||
Get the main character name from a user
|
||||
|
||||
:param user:
|
||||
:type user:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
if user is None:
|
||||
sentinel_user = get_sentinel_user()
|
||||
|
||||
return sentinel_user.username
|
||||
|
||||
main_character = get_main_character_from_user(user=user)
|
||||
|
||||
try:
|
||||
username = main_character.character_name
|
||||
except AttributeError:
|
||||
return str(user)
|
||||
|
||||
return username
|
||||
|
||||
|
||||
def get_sentinel_user() -> User:
|
||||
"""
|
||||
Get the sentinel user or create one
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
return User.objects.get_or_create(username="deleted")[0]
|
||||
14
allianceauth/framework/apps.py
Normal file
14
allianceauth/framework/apps.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Framework App Config
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrameworkConfig(AppConfig):
|
||||
"""
|
||||
Framework App Config
|
||||
"""
|
||||
|
||||
name = "allianceauth.framework"
|
||||
label = "framework"
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Alliance Auth CSS Framework
|
||||
*
|
||||
* This provides some CSS classes together with a couple of Bootstrap fixes
|
||||
* to be used throughout Alliance Auth and its Community Apps
|
||||
*/
|
||||
|
||||
/* Bootstrap fixes
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
.table {
|
||||
--bs-table-bg: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Side Navigation
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
#sidebar > div {
|
||||
width: 325px;
|
||||
}
|
||||
|
||||
/* Menu items in general */
|
||||
#sidebar-menu li > a,
|
||||
#sidebar-menu li > ul > li > a {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 210px;
|
||||
}
|
||||
|
||||
/* Parent items with chevron and possible badge */
|
||||
#sidebar-menu li:has(span.badge) > a[data-bs-toggle="collapse"] {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
/* Child items with possible badge */
|
||||
#sidebar-menu li > ul > li > a {
|
||||
max-width: 189px;
|
||||
}
|
||||
|
||||
/* Chevron icons */
|
||||
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-down,
|
||||
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-right {
|
||||
display: block;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-right,
|
||||
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-down {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cursor classes
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
.cursor-auto {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-wait {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.cursor-text {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.cursor-move {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cursor-inherit {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.cursor-zoom-in {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.cursor-zoom-out {
|
||||
cursor: zoom-out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Callouts
|
||||
*
|
||||
* Not quite alerts, but custom and helpful notes for folks.
|
||||
* Requires a base and modifier class.
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
/* Common styles for all types */
|
||||
.aa-callout {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-left-width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.aa-callout.aa-callout-sm {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.aa-callout.aa-callout-lg {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Last item bottom margin should be 0 */
|
||||
.aa-callout :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Variations */
|
||||
.aa-callout.aa-callout-danger {
|
||||
border-left-color: var(--bs-danger);
|
||||
}
|
||||
|
||||
.aa-callout.aa-callout-info {
|
||||
border-left-color: var(--bs-info);
|
||||
}
|
||||
|
||||
.aa-callout.aa-callout-success {
|
||||
border-left-color: var(--bs-success);
|
||||
}
|
||||
|
||||
.aa-callout.aa-callout-warning {
|
||||
border-left-color: var(--bs-warning);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{#Usage:#}
|
||||
{# {% include "framework/dashboard/widget-title.html" with title="Foobar" %}#}
|
||||
|
||||
<div class="text-center">
|
||||
<h4 class="ms-auto me-auto mb-3">
|
||||
{{ title }}
|
||||
</h4>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
{#Usage:#}
|
||||
{# {% include "framework/header/page-header.html" with title="Foobar" subtitle="Barfoo" %}#}
|
||||
|
||||
{% if title %}
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{{ title }}
|
||||
|
||||
{% if subtitle %}
|
||||
<br>
|
||||
<small class="text-muted">{{ subtitle }}</small>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% endif %}
|
||||
3
allianceauth/framework/tests/__init__.py
Normal file
3
allianceauth/framework/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Initializing our tests
|
||||
"""
|
||||
179
allianceauth/framework/tests/test_api_user.py
Normal file
179
allianceauth/framework/tests/test_api_user.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Test sentinel user
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# Django
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
# Alliance Auth
|
||||
from allianceauth.framework.api.user import (
|
||||
get_sentinel_user,
|
||||
get_main_character_from_user,
|
||||
get_main_character_name_from_user
|
||||
)
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
|
||||
class TestSentinelUser(TestCase):
|
||||
"""
|
||||
Tests for the sentinel user
|
||||
"""
|
||||
|
||||
def test_should_create_user_when_it_does_not_exist(self) -> None:
|
||||
"""
|
||||
Test should create a sentinel user when it doesn't exist
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
# when
|
||||
user = get_sentinel_user()
|
||||
|
||||
# then
|
||||
self.assertEqual(first=user.username, second="deleted")
|
||||
|
||||
def test_should_return_user_when_it_does(self) -> None:
|
||||
"""
|
||||
Test should return sentinel user when it exists
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
# given
|
||||
User.objects.create_user(username="deleted")
|
||||
|
||||
# when
|
||||
user = get_sentinel_user()
|
||||
|
||||
# then
|
||||
self.assertEqual(first=user.username, second="deleted")
|
||||
|
||||
|
||||
class TestGetMainForUser(TestCase):
|
||||
"""
|
||||
Tests for get_main_character_from_user
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
"""
|
||||
Set up groups and users
|
||||
"""
|
||||
|
||||
super().setUpClass()
|
||||
|
||||
cls.character_name = "William T. Riker"
|
||||
cls.character_name_2 = "Christopher Pike"
|
||||
|
||||
cls.username = re.sub(pattern=r"[^\w\d@\.\+-]", repl="_", string=cls.character_name)
|
||||
cls.username_2 = re.sub(
|
||||
pattern=r"[^\w\d@\.\+-]", repl="_", string=cls.character_name_2
|
||||
)
|
||||
|
||||
cls.user = AuthUtils.create_user(username=cls.username)
|
||||
cls.user_without_main = AuthUtils.create_user(
|
||||
username=cls.username_2, disconnect_signals=True
|
||||
)
|
||||
|
||||
cls.character = AuthUtils.add_main_character_2(
|
||||
user=cls.user, name=cls.character_name, character_id=1001
|
||||
)
|
||||
|
||||
|
||||
def test_get_main_character_from_user_should_return_character_name(self):
|
||||
"""
|
||||
Test should return the main character name for a regular user
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
character = get_main_character_from_user(user=self.user)
|
||||
|
||||
self.assertEqual(first=character, second=self.character)
|
||||
|
||||
|
||||
def test_get_main_character_from_user_should_return_none_for_no_main_character(self):
|
||||
"""
|
||||
Test should return None for User without a main character
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
character = get_main_character_from_user(user=self.user_without_main)
|
||||
|
||||
self.assertIsNone(obj=character)
|
||||
|
||||
|
||||
def test_get_main_character_from_user_should_none(self):
|
||||
"""
|
||||
Test should return None when user is None
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
user = None
|
||||
|
||||
character = get_main_character_from_user(user=user)
|
||||
|
||||
self.assertIsNone(obj=character)
|
||||
|
||||
|
||||
def test_get_main_character_name_from_user_should_return_character_name(self):
|
||||
"""
|
||||
Test should return the main character name for a regular user
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
character_name = get_main_character_name_from_user(user=self.user)
|
||||
|
||||
self.assertEqual(first=character_name, second=self.character_name)
|
||||
|
||||
def test_get_main_character_name_from_user_should_return_user_name(self):
|
||||
"""
|
||||
Test should return just the username for a user without a main character
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
character_name = get_main_character_name_from_user(user=self.user_without_main)
|
||||
|
||||
self.assertEqual(first=character_name, second=self.username_2)
|
||||
|
||||
def test_get_main_character_name_from_user_should_return_sentinel_user(self):
|
||||
"""
|
||||
Test should return "deleted" as username (Sentinel User)
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
user = get_sentinel_user()
|
||||
|
||||
character_name = get_main_character_name_from_user(user=user)
|
||||
|
||||
self.assertEqual(first=character_name, second="deleted")
|
||||
|
||||
def test_get_main_character_name_from_user_should_return_sentinel_user_for_none(self):
|
||||
"""
|
||||
Test should return "deleted" (Sentinel User) if user is None
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
user = None
|
||||
|
||||
character_name = get_main_character_name_from_user(user=user)
|
||||
|
||||
self.assertEqual(first=character_name, second="deleted")
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from allianceauth.menu.hooks import MenuItemHook
|
||||
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from allianceauth.services.hooks import UrlHook
|
||||
from allianceauth import hooks
|
||||
|
||||
from . import urls
|
||||
@@ -15,7 +16,7 @@ class GroupManagementMenuItem(MenuItemHook):
|
||||
MenuItemHook.__init__(
|
||||
self,
|
||||
text=_("Group Management"),
|
||||
classes="fas fa-users-cog fa-fw",
|
||||
classes="fa-solid fa-users-gear",
|
||||
url_name="groupmanagement:management",
|
||||
order=50,
|
||||
navactive=[
|
||||
@@ -33,11 +34,43 @@ class GroupManagementMenuItem(MenuItemHook):
|
||||
return ""
|
||||
|
||||
|
||||
"""
|
||||
<li class="d-flex m-2 p-2 pt-0 pb-0 mt-0 mb-0">
|
||||
<i class="fa-solid fa-users fa-fw align-self-center me-2"></i>
|
||||
<a class="nav-link flex-fill align-self-center {% navactive request 'groupmanagement:groups' %}" href="{% url 'groupmanagement:groups' %}">
|
||||
{% translate "Groups" %}
|
||||
</a>
|
||||
</li>
|
||||
"""
|
||||
|
||||
|
||||
class GroupsMenuItem(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(
|
||||
self,
|
||||
text=_("Groups"),
|
||||
classes="fa-solid fa-user",
|
||||
url_name="groupmanagement:groups",
|
||||
order=25,
|
||||
navactive=[
|
||||
"groupmanagement:groups", # group list view
|
||||
],
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
return MenuItemHook.render(self, request)
|
||||
|
||||
|
||||
@hooks.register("menu_item_hook")
|
||||
def register_menu():
|
||||
def register_manager_menu():
|
||||
return GroupManagementMenuItem()
|
||||
|
||||
|
||||
@hooks.register("menu_item_hook")
|
||||
def register_groups_menu():
|
||||
return GroupsMenuItem()
|
||||
|
||||
|
||||
@hooks.register("url_hook")
|
||||
def register_urls():
|
||||
return UrlHook(urls, "group", r"^groups/")
|
||||
|
||||
@@ -74,11 +74,11 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='AuthGroup',
|
||||
fields=[
|
||||
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='auth.Group')),
|
||||
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='auth.Group')),
|
||||
('internal', models.BooleanField(default=True, help_text='Internal group, users cannot see, join or request to join this group.<br>Used for groups such as Members, Corp_*, Alliance_* etc.<br><b>Overrides Hidden and Open options when selected.</b>')),
|
||||
('hidden', models.BooleanField(default=True, help_text='Group is hidden from users but can still join with the correct link.')),
|
||||
('open', models.BooleanField(default=False, help_text='Group is open and users will be automatically added upon request. <br>If the group is not open users will need their request manually approved.')),
|
||||
('description', models.CharField(max_length=512, blank=True, help_text='Description of the group shown to users.', )),
|
||||
('description', models.CharField(max_length=512, blank=True, help_text='Description of the group shown to users.', )),
|
||||
|
||||
('group_leaders', models.ManyToManyField(related_name='leads_groups', to=settings.AUTH_USER_MODEL, blank=True, help_text='Group leaders can process group requests for this group specifically. Use the auth.group_management permission to allow a user to manage all groups.',)),
|
||||
],
|
||||
|
||||
@@ -1,135 +1,132 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load i18n %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% block page_title %}{{ group }} {% translate "Audit Log" %}{% endblock page_title %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load navactive %}
|
||||
|
||||
{% block page_title %}
|
||||
{{ group }} {% translate "Audit Log" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Audit Log" %} - {{ group.name }}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block header_nav_collapse_left %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Back" %}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<br>
|
||||
{% include 'groupmanagement/menu.html' %}
|
||||
{% if entries %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="log-entries">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% translate "Date/Time" %}</th>
|
||||
<th scope="col">{% translate "Requestor" %}</th>
|
||||
<th scope="col">{% translate "Character" %}</th>
|
||||
<th scope="col">{% translate "Corporation" %}</th>
|
||||
<th scope="col">{% translate "Type" %}</th>
|
||||
<th scope="col">{% translate "Action" %}</th>
|
||||
<th scope="col">{% translate "Actor" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ group }} - {% translate "Audit Log" %}
|
||||
</div>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>{{ entry.date|date:"Y-M-d, H:i" }}</td>
|
||||
<td>{{ entry.requestor }}</td>
|
||||
<td>{{ entry.req_char }}</td>
|
||||
<td>{{ entry.req_char.corporation_name }}</td>
|
||||
<td>{{ entry.type_to_str }}</td>
|
||||
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'groupmanagement:membership' %}" role="button">
|
||||
{% translate "Back" %}
|
||||
</a>
|
||||
</p>
|
||||
{% if entry.request_type is None %}
|
||||
<td>{% translate "Removed" %}</td>
|
||||
{% else %}
|
||||
<td>{{ entry.action_to_str }}</td>
|
||||
{% endif %}
|
||||
|
||||
{% if entries %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="log-entries">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% translate "Date/Time" %}</th>
|
||||
<th scope="col">{% translate "Requestor" %}</th>
|
||||
<th scope="col">{% translate "Character" %}</th>
|
||||
<th scope="col">{% translate "Corporation" %}</th>
|
||||
<th scope="col">{% translate "Type" %}</th>
|
||||
<th scope="col">{% translate "Action" %}</th>
|
||||
<th scope="col">{% translate "Actor" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<td>{{ entry.request_actor }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>{{ entry.date|date:"Y-M-d, H:i" }}</td>
|
||||
<td>{{ entry.requestor }}</td>
|
||||
<td>{{ entry.req_char }}</td>
|
||||
<td>{{ entry.req_char.corporation_name }}</td>
|
||||
<td>{{ entry.type_to_str }}</td>
|
||||
|
||||
{% if entry.request_type is None %}
|
||||
<td>{% translate "Removed" %}</td>
|
||||
{% else %}
|
||||
<td>{{ entry.action_to_str }}</td>
|
||||
{% endif %}
|
||||
|
||||
<td>{{ entry.request_actor }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="text-muted">
|
||||
{% translate "All times displayed are EVE/UTC." %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No entries found for this group." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
{% translate "All times displayed are EVE/UTC." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No entries found for this group." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
{% include 'bundles/moment-js.html' with locale=True %}
|
||||
{% include 'bundles/filterdropdown-js.html' %}
|
||||
|
||||
<script>
|
||||
$.fn.dataTable.moment = (format, locale) => {
|
||||
const types = $.fn.dataTable.ext.type;
|
||||
|
||||
// Add type detection
|
||||
types.detect.unshift((d) => {
|
||||
return moment(d, format, locale, true).isValid() ?
|
||||
'moment-'+format :
|
||||
null;
|
||||
});
|
||||
|
||||
// Add sorting method - use an integer for the sorting
|
||||
types.order[ 'moment-'+format+'-pre' ] = (d) => {
|
||||
return moment(d, format, locale, true).unix();
|
||||
};
|
||||
};
|
||||
|
||||
$(document).ready(() => {
|
||||
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
|
||||
|
||||
$('#log-entries').DataTable({
|
||||
order: [[0, 'desc'], [1, 'asc']],
|
||||
filterDropDown:
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
idx: 1
|
||||
},
|
||||
{
|
||||
idx: 2
|
||||
},
|
||||
{
|
||||
idx: 3
|
||||
},
|
||||
{
|
||||
idx: 4
|
||||
},
|
||||
{
|
||||
idx: 5
|
||||
},
|
||||
{
|
||||
idx: 6
|
||||
}
|
||||
],
|
||||
bootstrap: true,
|
||||
bootstrap_version: 5
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
$.fn.dataTable.moment = function(format, locale) {
|
||||
let types = $.fn.dataTable.ext.type;
|
||||
|
||||
// Add type detection
|
||||
types.detect.unshift(function(d) {
|
||||
return moment(d, format, locale, true).isValid() ?
|
||||
'moment-'+format :
|
||||
null;
|
||||
});
|
||||
|
||||
// Add sorting method - use an integer for the sorting
|
||||
types.order[ 'moment-'+format+'-pre' ] = function(d) {
|
||||
return moment(d, format, locale, true).unix();
|
||||
};
|
||||
};
|
||||
|
||||
$(document).ready(function(){
|
||||
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
|
||||
|
||||
$('#log-entries').DataTable({
|
||||
order: [[0, 'desc'], [1, 'asc']],
|
||||
filterDropDown:
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
idx: 1
|
||||
},
|
||||
{
|
||||
idx: 2
|
||||
},
|
||||
{
|
||||
idx: 3
|
||||
},
|
||||
{
|
||||
idx: 4
|
||||
},
|
||||
{
|
||||
idx: 5
|
||||
},
|
||||
{
|
||||
idx: 6
|
||||
}
|
||||
],
|
||||
bootstrap: true
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
{% include 'bundles/datatables-css-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,111 +1,108 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load evelinks %}
|
||||
{% load navactive %}
|
||||
|
||||
{% block page_title %}{% translate "Group Members" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Group Members" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Group Members" %} - {{ group.name }}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block header_nav_collapse_left %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Back" %}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<br>
|
||||
{% include 'groupmanagement/menu.html' %}
|
||||
{% if group.user_set %}
|
||||
<div class="table-responsive">
|
||||
<table class="table" id="tab_group_members">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ group.name }} - {% translate 'Members' %}
|
||||
</div>
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ member.main_char|character_portrait_url:32 }}" class="rounded-circle" style="margin-right: 1rem;" alt="{{ member.main_char.character_name }}">
|
||||
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<a class="btn btn-default" href="{% url 'groupmanagement:membership' %}" role="button">
|
||||
{% translate "Back" %}
|
||||
</a>
|
||||
</p>
|
||||
{% if member.main_char %}
|
||||
<a href="{{ member.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ member.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ member.user.username }}
|
||||
{% endif %}
|
||||
|
||||
{% if group.user_set %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa" id="tab_group_members">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% if member.is_leader %}
|
||||
<sup><i class="fa-solid fa-star" title="{% translate "Group leader" %}"></i></sup>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td>
|
||||
<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 %}
|
||||
<a href="{{ member.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ member.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ member.user.username }}
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if member.main_char %}
|
||||
<a href="{{ member.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ member.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ member.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% if member.is_leader %}
|
||||
<i class="fas fa-star" title="{% translate "Group leader" %}" style="margin-left: 1rem;"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if member.main_char %}
|
||||
<a href="{{ member.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ member.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ member.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:membership_remove' group.id member.user.id %}" class="btn btn-danger" title="{% translate "Remove from group" %}">
|
||||
<i class="glyphicon glyphicon-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="text-muted">
|
||||
<i class="fas fa-star"></i>: {% translate "Group leader" %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No group members to list." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'groupmanagement:membership_remove' group.id member.user.id %}" class="btn btn-danger" title="{% translate "Remove from group" %}">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="text-muted">
|
||||
<i class="fa-solid fa-star"></i>: {% translate "Group leader" %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No group members to list." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#tab_group_members').DataTable({
|
||||
order: [[0, "asc"]],
|
||||
columnDefs: [
|
||||
{
|
||||
"sortable": false,
|
||||
"targets": [2]
|
||||
},
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
$(document).ready(function(){
|
||||
$('#tab_group_members').DataTable({
|
||||
order: [[0, "asc"]],
|
||||
columnDefs: [
|
||||
{
|
||||
"sortable": false,
|
||||
"targets": [2]
|
||||
},
|
||||
],
|
||||
"stateSave": true,
|
||||
"stateDuration": 0
|
||||
});
|
||||
});
|
||||
{% include 'bundles/datatables-css-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,88 +1,86 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load navactive %}
|
||||
|
||||
{% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %}
|
||||
{% block header_nav_brand %}{% translate "Groups Membership" %}{% endblock header_nav_brand %}
|
||||
|
||||
{% block extra_css %}{% endblock extra_css %}
|
||||
|
||||
{% block header_nav_collapse_left %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Join/Leave Requests" %}</a>
|
||||
</li>
|
||||
{% endblock header_nav_collapse_left %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<br>
|
||||
{% include 'groupmanagement/menu.html' %}
|
||||
{% if groups %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Description" %}</th>
|
||||
<th>{% translate "Status" %}</th>
|
||||
<th style="white-space: nowrap;" class="text-center">{% translate "Member Count" %}</th>
|
||||
<th style="min-width: 170px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% translate "Groups" %}
|
||||
</div>
|
||||
<tbody class="align-middle">
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'groupmanagement:membership' group.id %}">{{ group.name }}</a>
|
||||
</td>
|
||||
|
||||
<div class="panel-body">
|
||||
{% if groups %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Description" %}</th>
|
||||
<th>{% translate "Status" %}</th>
|
||||
<th style="white-space: nowrap;">{% translate "Member Count" %}</th>
|
||||
<th style="min-width: 170px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<td>{{ group.authgroup.description|linebreaks|urlize }}</td>
|
||||
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'groupmanagement:membership' group.id %}">{{ group.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if group.authgroup.hidden %}
|
||||
<span class="badge bg-info">{% translate "Hidden" %}</span>
|
||||
{% endif %}
|
||||
{% if group.authgroup.open %}
|
||||
<span class="badge bg-success">{% translate "Open" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% translate "Requestable" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>{{ group.authgroup.description|linebreaks|urlize }}</td>
|
||||
<td class="text-center">
|
||||
{{ group.num_members }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if group.authgroup.hidden %}
|
||||
<span class="label label-info">{% translate "Hidden" %}</span>
|
||||
{% elif group.authgroup.open %}
|
||||
<span class="label label-success">{% translate "Open" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-default">{% translate "Requestable" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'groupmanagement:membership' group.id %}" class="btn btn-primary" title="{% translate "View Members" %}">
|
||||
<i class="fa-regular fa-eye"></i>
|
||||
</a>
|
||||
|
||||
<td class="text-right">
|
||||
{{ group.num_members }}
|
||||
</td>
|
||||
<a href="{% url "groupmanagement:audit_log" group.id %}" class="btn btn-info" title="{% translate "Audit Members" %}">
|
||||
<i class="fa-regular fa-rectangle-list"></i>
|
||||
</a>
|
||||
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:membership' group.id %}" class="btn btn-primary" title="{% translate "View Members" %}">
|
||||
<i class="glyphicon glyphicon-eye-open"></i>
|
||||
</a>
|
||||
|
||||
<a href="{% url "groupmanagement:audit_log" group.id %}" class="btn btn-info" title="{% translate "Audit Members" %}">
|
||||
<i class="glyphicon glyphicon-list-alt"></i>
|
||||
</a>
|
||||
|
||||
<a id="clipboard-copy" data-clipboard-text="{{ SITE_URL }}{% url 'groupmanagement:request_add' group.id %}" class="btn btn-warning" title="{% translate "Copy Direct Join Link" %}">
|
||||
<i class="glyphicon glyphicon-copy"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No groups to list." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<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" %}">
|
||||
<i class="fa-regular fa-clipboard"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No groups to list." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/clipboard-js.html' %}
|
||||
{% include "bundles/clipboard-js.html" %}
|
||||
|
||||
<script>
|
||||
new ClipboardJS('#clipboard-copy');
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
@@ -1,62 +1,115 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Available Groups" %}{% endblock page_title %}
|
||||
{% block extra_css %}{% endblock extra_css %}
|
||||
{% block page_title %}
|
||||
{% translate "Available Groups" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Available Groups" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% if manager_perms %}
|
||||
{% block header_nav_collapse_left %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}
|
||||
{% if req_count %}
|
||||
<span class="badge bg-secondary">{{ req_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Available Groups" %}</h1>
|
||||
{% if groups %}
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Description" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% if groups %}
|
||||
<table class="table" id="groupsTable" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Description" %}</th>
|
||||
<th>
|
||||
{% translate "Leaders" %}<br>
|
||||
<span class="my-1 me-1 fw-lighter badge bg-primary">{% translate "User" %}</span>
|
||||
<span class="my-1 me-1 fw-lighter badge bg-secondary">{% translate "Group" %}</span>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for g in groups %}
|
||||
<tr>
|
||||
<td>{{ g.group.name }}</td>
|
||||
<td>{{ g.group.authgroup.description|linebreaks|urlize }}</td>
|
||||
<td class="text-right">
|
||||
{% if g.group in user.groups.all %}
|
||||
{% if not g.request %}
|
||||
<a href="{% url 'groupmanagement:request_leave' g.group.id %}" class="btn btn-danger">
|
||||
{% translate "Leave" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" disabled>
|
||||
{% translate "Pending" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif not g.request %}
|
||||
{% if g.group.authgroup.open %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-success">
|
||||
{% translate "Join" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-primary">
|
||||
{% translate "Request" %}
|
||||
</a>
|
||||
<tbody class>
|
||||
{% for g in groups %}
|
||||
<tr>
|
||||
<td>{{ g.group.name }}</td>
|
||||
<td>
|
||||
{% if g.group.authgroup.description %}
|
||||
{{ g.group.authgroup.description|linebreaks|urlize }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="max-width: 30%;">
|
||||
{% if g.group.authgroup.group_leaders.all.count %}
|
||||
{% for leader in g.group.authgroup.group_leaders.all %}
|
||||
{% if leader.profile.main_character %}
|
||||
<span class="my-1 me-1 badge bg-primary">{{leader.profile.main_character}}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if g.group.authgroup.group_leaders.all.count %}
|
||||
{% for group in g.group.authgroup.group_leader_groups.all %}
|
||||
<span class="my-1 me-1 badge bg-secondary">{{group.name}}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{% if g.group in user_groups %}
|
||||
{% if not g.request %}
|
||||
<a href="{% url 'groupmanagement:request_leave' g.group.id %}" class="btn btn-danger">
|
||||
{% translate "Leave" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" disabled>
|
||||
{% translate "Pending" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No groups available." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif not g.request %}
|
||||
{% if g.group.authgroup.open %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-success">
|
||||
{% translate "Join" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-primary">
|
||||
{% translate "Request" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" disabled>
|
||||
{% translate "Pending" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
{% translate "No groups available." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$('#groupsTable').DataTable();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css-bs5.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,166 +1,174 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load evelinks %}
|
||||
{% load navactive %}
|
||||
|
||||
{% block page_title %}{% translate "Groups Management" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Groups Management" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Groups Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block header_nav_collapse_left %}
|
||||
<li class="active">
|
||||
<a class="nav-link active" id="add-tab" data-bs-toggle="tab" data-bs-target="#add" type="button" role="tab" aria-controls="add" aria-selected="true">
|
||||
{% translate "Join Requests" %}
|
||||
|
||||
{% if acceptrequests %}
|
||||
<span class="badge bg-secondary">{{ acceptrequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if not show_leave_tab %}
|
||||
<li>
|
||||
<a class="nav-link" id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave" type="button" role="tab" aria-controls="leave" aria-selected="false">
|
||||
{% translate "Leave Requests" %}
|
||||
|
||||
{% if leaverequests %}
|
||||
<span class="badge bg-secondary">{{ leaverequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link {% navactive request 'groupmanagement:membership groupmanagement:audit_log' %}" href="{% url 'groupmanagement:membership' %}">
|
||||
{% translate "Group Membership" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock header_nav_collapse_left %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.nav-tabs > li.active > a {
|
||||
background-color: rgb(236, 240, 241) !important;
|
||||
color: rgb(44, 62, 80);
|
||||
}
|
||||
</style>
|
||||
{% endblock extra_css %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<br>
|
||||
{% include 'groupmanagement/menu.html' %}
|
||||
<div class="tab-content">
|
||||
<div id="add" class="tab-pane active">
|
||||
{% if acceptrequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th>{% translate "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a data-toggle="tab" href="#add">
|
||||
{% translate "Join Requests" %}
|
||||
<tbody class="align-middle">
|
||||
{% for acceptrequest in acceptrequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="rounded-circle" style="margin-right: 1rem;" alt="{{ acceptrequest.main_char.character_name }}">
|
||||
|
||||
{% if acceptrequests %}
|
||||
<span class="badge">{{ acceptrequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ acceptrequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% if not show_leave_tab %}
|
||||
<li>
|
||||
<a data-toggle="tab" href="#leave">
|
||||
{% translate "Leave Requests" %}
|
||||
<td>
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.corporation_name }}
|
||||
</a>
|
||||
<br>
|
||||
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% if leaverequests %}
|
||||
<span class="badge">{{ leaverequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<td>{{ acceptrequest.group.name }}</td>
|
||||
|
||||
<div class="panel panel-default panel-tabs-aa">
|
||||
<div class="panel-body">
|
||||
<div class="tab-content">
|
||||
|
||||
<div id="add" class="tab-pane active">
|
||||
{% if acceptrequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th>{% translate "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for acceptrequest in acceptrequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<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 %}
|
||||
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ acceptrequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ acceptrequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
|
||||
{% translate "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
|
||||
{% translate "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No group add requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not show_leave_tab %}
|
||||
<div id="leave" class="tab-pane">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th>{% translate "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for leaverequest in leaverequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<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 %}
|
||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ leaverequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ leaverequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
|
||||
{% translate "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
|
||||
{% translate "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No group leave requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<td class="text-end">
|
||||
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
|
||||
{% translate "Accept" %}
|
||||
</a>
|
||||
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
|
||||
{% translate "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="aa-callout aa-callout-warning text-center">
|
||||
{% translate "No group add requests." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not show_leave_tab %}
|
||||
<div id="leave" class="tab-pane">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Character" %}</th>
|
||||
<th>{% translate "Organization" %}</th>
|
||||
<th>{% translate "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="align-middle">
|
||||
{% for leaverequest in leaverequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="rounded-circle" style="margin-right: 1rem;" alt="{{ leaverequest.main_char.character_name }}">
|
||||
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ leaverequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.corporation_name }}
|
||||
</a>
|
||||
<br>
|
||||
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% translate "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>{{ leaverequest.group.name }}</td>
|
||||
|
||||
<td class="text-end">
|
||||
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
|
||||
{% translate "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
|
||||
{% translate "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="aa-callout aa-callout-warning text-center">{% translate "No group leave requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
{% load i18n %}
|
||||
{% load navactive %}
|
||||
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
||||
<span class="sr-only">{% translate "Toggle navigation" %}</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="{% navactive request 'groupmanagement:management' %}">
|
||||
<a href="{% url 'groupmanagement:management' %}">{% translate "Group Requests" %}</a>
|
||||
</li>
|
||||
<li class="{% navactive request 'groupmanagement:membership groupmanagement:audit_log' %}">
|
||||
<a href="{% url 'groupmanagement:membership' %}">{% translate "Group Membership" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Group Requests" %}</a>
|
||||
</li>
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link {% navactive request 'groupmanagement:membership groupmanagement:audit_log' %}" href="{% url 'groupmanagement:membership' %}">{% translate "Group Membership" %}</a>
|
||||
</li>
|
||||
|
||||
@@ -66,7 +66,7 @@ class TestViews(TestCase):
|
||||
content = response_content_to_str(response)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertIn('id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave"', content)
|
||||
self.assertIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
|
||||
@@ -83,7 +83,7 @@ class TestViews(TestCase):
|
||||
content = response_content_to_str(response)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertNotIn('id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave"', content)
|
||||
self.assertNotIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
|
||||
@@ -99,5 +99,5 @@ class TestViews(TestCase):
|
||||
# then
|
||||
content = response_content_to_str(response)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertIn('<a class="nav-link" id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave"', content)
|
||||
self.assertIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@@ -90,7 +90,7 @@ def group_membership_audit(request, group_id):
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Group does not exist")
|
||||
render_items = {'group': group.name}
|
||||
render_items = {'group': group}
|
||||
entries = RequestLog.objects.filter(group=group).order_by('-date')
|
||||
render_items['entries'] = entries
|
||||
|
||||
@@ -314,8 +314,10 @@ def groups_view(request):
|
||||
groups_qs = GroupManager.get_joinable_groups_for_user(
|
||||
request.user, include_hidden=False
|
||||
)
|
||||
groups_qs = groups_qs.order_by('name')
|
||||
groups_qs = groups_qs.order_by('name').select_related("authgroup").prefetch_related('authgroup__group_leaders', 'authgroup__group_leaders__profile__main_character', 'authgroup__group_leader_groups')
|
||||
groups = []
|
||||
|
||||
## TODO see about making this faster
|
||||
for group in groups_qs:
|
||||
group_request = GroupRequest.objects\
|
||||
.filter(user=request.user)\
|
||||
@@ -325,7 +327,14 @@ def groups_view(request):
|
||||
'request': group_request[0] if group_request else None
|
||||
})
|
||||
|
||||
context = {'groups': groups}
|
||||
count = 0
|
||||
perms = GroupManager.can_manage_groups(request.user)
|
||||
if perms:
|
||||
count = GroupManager.pending_requests_count_for_user(request.user)
|
||||
|
||||
user_groups_list = list(request.user.groups.all())
|
||||
context = {'groups': groups, "manager_perms": perms, "req_count":count, "user_groups": user_groups_list}
|
||||
|
||||
return render(request, 'groupmanagement/groups.html', context=context)
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def get_app_modules():
|
||||
|
||||
|
||||
def get_app_submodules(module_name):
|
||||
"""
|
||||
"""pyt
|
||||
Get a specific sub module of the app
|
||||
:param module_name: module name to get
|
||||
:return: name, module tuple
|
||||
@@ -122,3 +122,17 @@ def get_hooks(name):
|
||||
"""
|
||||
register_all_hooks()
|
||||
return _hooks.get(name, [])
|
||||
|
||||
|
||||
class DashboardItemHook:
|
||||
def __init__(self, view_function, order:int=10):
|
||||
self.view_function = view_function
|
||||
self.order = order
|
||||
|
||||
def render(self, request):
|
||||
try:
|
||||
logger.debug(f"Rendering {self.view_function} to dashboard")
|
||||
return self.view_function(request)
|
||||
except Exception as e:
|
||||
logger.exception(f"Rendering {self.view_function} failed!")
|
||||
return ""
|
||||
|
||||
@@ -13,7 +13,7 @@ class ChoiceInline(admin.TabularInline):
|
||||
@admin.register(ApplicationQuestion)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['title', 'help_text', 'multi_select']}),
|
||||
(None, {'fields': ['title', 'help_text', 'multi_select']}),
|
||||
]
|
||||
inlines = [ChoiceInline]
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allianceauth import hooks
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from allianceauth.menu.hooks import MenuItemHook
|
||||
from allianceauth.services.hooks import UrlHook
|
||||
|
||||
from . import urls
|
||||
from .models import Application
|
||||
@@ -12,7 +13,7 @@ class ApplicationsMenu(MenuItemHook):
|
||||
MenuItemHook.__init__(
|
||||
self,
|
||||
_('Applications'),
|
||||
'far fa-file fa-fw',
|
||||
'fa-regular fa-file',
|
||||
'hrapplications:index',
|
||||
navactive=['hrapplications:'])
|
||||
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %}
|
||||
{% block page_title %}
|
||||
{% translate "Choose a Corp" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Choose a Corp" %}</h1>
|
||||
<div>
|
||||
{% translate "Choose a Corp" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
{% if choices %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">{% translate "Available Corps" %}</div>
|
||||
<table class="table table-responsive">
|
||||
{% for choice in choices %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:create_view' choice.0 %}" class="btn btn-primary" title="Apply">{{ choice.1 }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{% translate "Available Corps" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table table-responsive">
|
||||
{% for choice in choices %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:create_view' choice.0 %}" class="btn btn-primary" title="Apply">{{ choice.1 }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">{% translate "No corps are accepting applications at this time." %}</div>
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Apply To" %} {{ corp.corporation_name }}</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="row">
|
||||
<form class="form-signin">
|
||||
{% csrf_token %}
|
||||
{% for question in questions %}
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
|
||||
<div class=" ">
|
||||
{% if question.help_text %}
|
||||
<div class="text-center">{{ question.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for choice in question.choices.all %}
|
||||
<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="id_{{ question.pk }}_choice_{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
|
||||
{% empty %}
|
||||
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
|
||||
<div>
|
||||
<h1 class="page-header text-center mb-3">
|
||||
{% translate "Apply To" %} {{ corp.corporation_name }}
|
||||
</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
{% translate "Application form" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for question in questions %}
|
||||
<div class="card mb-3 form-group border-0">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{{ question.title }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if question.help_text %}
|
||||
<p class="text-muted">
|
||||
{{ question.help_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% for choice in question.choices.all %}
|
||||
<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="id_{{ question.pk }}_choice_{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
|
||||
{% empty %}
|
||||
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="10"></textarea>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix">
|
||||
{% translate "Submit" as button_text %}
|
||||
{% bootstrap_button button_class="btn btn-primary" content=button_text name="submitApplicationForm" id="submitApplicationForm" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% translate "Submit" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,193 +1,215 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}
|
||||
{% block extra_css %}{% endblock extra_css %}
|
||||
{% block page_title %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "Personal Applications" %}
|
||||
<div class="text-right">
|
||||
{% if create %}
|
||||
<a href="{% url 'hrapplications:create_view' %}">
|
||||
<button type="button" class="btn btn-success">{% translate "Create Application" %}</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-success" disabled>{% translate "Create Application" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</h1>
|
||||
{% if personal_apps %}
|
||||
<div class="panel panel-default">
|
||||
<table class="table table-condensed">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
{% for personal_app in personal_apps %}
|
||||
<tr>
|
||||
<td class="text-center">{{ personal_app.user.username }}</td>
|
||||
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
|
||||
<td class="text-center">
|
||||
{% if personal_app.approved == None %}
|
||||
<div class="label label-warning">{% translate "Pending" %}</div>
|
||||
{% elif personal_app.approved == True %}
|
||||
<div class="label label-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="label label-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:personal_view' personal_app.id %}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-eye-open"></span>
|
||||
</a>
|
||||
<div>
|
||||
{% translate "Personal Applications" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
{% if personal_app.approved == None %}
|
||||
<a href="{% url 'hrapplications:personal_removal' personal_app.id %}" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<div class="text-end mb-3">
|
||||
{% if create %}
|
||||
<a href="{% url 'hrapplications:create_view' %}">
|
||||
<button type="button" class="btn btn-success">{% translate "Create Application" %}</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-success" disabled>{% translate "Create Application" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if personal_apps %}
|
||||
<div class="card card-default mb-3">
|
||||
<div class="card-body">
|
||||
<table class="table table-condensed">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% for personal_app in personal_apps %}
|
||||
<tr>
|
||||
<td class="text-center">{{ personal_app.user.username }}</td>
|
||||
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
|
||||
<td class="text-center">
|
||||
{% if personal_app.approved == None %}
|
||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||
{% elif personal_app.approved == True %}
|
||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:personal_view' personal_app.id %}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
|
||||
{% if personal_app.approved == None %}
|
||||
<a href="{% url 'hrapplications:personal_removal' personal_app.id %}" class="btn btn-danger">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.auth.human_resources %}
|
||||
<h1 class="page-header text-center">{% translate "Application Management" %}
|
||||
<div class="text-right">
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#myModal">
|
||||
{% translate "Search Applications" %}
|
||||
</button>
|
||||
</div>
|
||||
</h1>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a data-toggle="tab" href="#pending">{% translate "Pending" %}</a></li>
|
||||
<li><a data-toggle="tab" href="#reviewed">{% translate "Reviewed" %}</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div id="pending" class="tab-pane fade in active panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% if applications %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Date" %}</th>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
{% for app in applications %}
|
||||
{% translate "Application Management" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#modal-hr-search">
|
||||
{% translate "Search Applications" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-body clearfix">
|
||||
<ul class="nav nav-tabs" id="application-list" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a
|
||||
class="nav-link active"
|
||||
id="pending"
|
||||
data-bs-toggle="tab"
|
||||
href="#tab-pending"
|
||||
role="tab"
|
||||
aria-controls="tab-pending"
|
||||
aria-selected="true"
|
||||
>
|
||||
{% translate "Pending" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" role="presentation">
|
||||
<a
|
||||
class="nav-link"
|
||||
id="reviewed"
|
||||
data-bs-toggle="tab"
|
||||
href="#tab-reviewed"
|
||||
role="tab"
|
||||
aria-controls="tab-reviewed"
|
||||
aria-selected="false"
|
||||
>
|
||||
{% translate "Reviewed" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="application-list-content">
|
||||
<div id="tab-pending" class="tab-pane fade show active" role="tabpanel" aria-labelledby="tab-pending">
|
||||
{% if applications %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center">{{ app.created }}</td>
|
||||
<td class="text-center">{{ app.user.username }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
<td class="text-center">{{ app.form.corp.corporation_name }}</td>
|
||||
<td class="text-center">
|
||||
{% if app.approved == None %}
|
||||
{% if app.reviewer_str %}
|
||||
<div class="label label-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||
{% else %}
|
||||
<div class="label label-warning">{% translate "Pending" %}</div>
|
||||
{% endif %}
|
||||
{% elif app.approved == True %}
|
||||
<div class="label label-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="label label-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:view' app.id %}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-eye-open"></span>
|
||||
</a>
|
||||
</td>
|
||||
<th class="text-center">{% translate "Date" %}</th>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No pending applications." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="reviewed" class="tab-pane fade panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% if finished_applications %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Date" %}</th>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
{% for app in finished_applications %}
|
||||
<tr>
|
||||
<td class="text-center">{{ app.created }}</td>
|
||||
<td class="text-center">{{ app.user.username }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
<td class="text-center">{{ app.form.corp.corporation_name }}</td>
|
||||
<td class="text-center">
|
||||
{% if app.approved == None %}
|
||||
{% if app.reviewer_str %}
|
||||
<div class="label label-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||
|
||||
{% for app in applications %}
|
||||
<tr>
|
||||
<td class="text-center">{{ app.created }}</td>
|
||||
<td class="text-center">{{ app.user.username }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
<td class="text-center">{{ app.form.corp.corporation_name }}</td>
|
||||
<td class="text-center">
|
||||
{% if app.approved == None %}
|
||||
{% if app.reviewer_str %}
|
||||
<div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||
{% else %}
|
||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||
{% endif %}
|
||||
{% elif app.approved == True %}
|
||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="label label-warning">{% translate "Pending" %}</div>
|
||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
{% elif app.approved == True %}
|
||||
<div class="label label-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="label label-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:view' app.id %}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-eye-open"></span>
|
||||
</a>
|
||||
{% if perms.hrapplications.delete_application %}
|
||||
<a href="{% url 'hrapplications:remove' app.id %}" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:view' app.id %}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No pending applications." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="tab-reviewed" class="tab-pane fade" role="tabpanel" aria-labelledby="tab-reviewed">
|
||||
{% if finished_applications %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Date" %}</th>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No reviewed applications." %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% for app in finished_applications %}
|
||||
<tr>
|
||||
<td class="text-center">{{ app.created }}</td>
|
||||
<td class="text-center">{{ app.user.username }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
<td class="text-center">{{ app.form.corp.corporation_name }}</td>
|
||||
<td class="text-center">
|
||||
{% if app.approved == None %}
|
||||
{% if app.reviewer_str %}
|
||||
<div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||
{% else %}
|
||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||
{% endif %}
|
||||
{% elif app.approved == True %}
|
||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'hrapplications:view' app.id %}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
|
||||
{% if perms.hrapplications.delete_application %}
|
||||
<a href="{% url 'hrapplications:remove' app.id %}" class="btn btn-danger">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "No reviewed applications." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.auth.human_resources %}
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">{% translate "Close" %}</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">{% translate "Application Search" %}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-signin" role="form" action="{% url 'hrapplications:search' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ search_form|bootstrap }}
|
||||
<br>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "hrapplications/partials/modals/search.html" %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if perms.auth.human_resources %}
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="modal-hr-search" tabindex="-1" aria-labelledby="modalHrSearch" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title fs-5" id="modalHrSearchLabel">
|
||||
{% translate "Application Search" %}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Close' %}"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="form-signin" role="form" action="{% url 'hrapplications:search' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form search_form %}
|
||||
|
||||
<div class="form-group mt-3 clearfix">
|
||||
{% translate "Search" as button_text %}
|
||||
{% bootstrap_button button_class="btn btn-primary" content=button_text name="search" id="search" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,49 +1,58 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}
|
||||
{% block extra_css %}{% endblock extra_css %}
|
||||
{% block page_title %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<div>
|
||||
{% if perms.auth.human_resources %}
|
||||
<h1 class="page-header text-center">{% translate "Application Search Results" %}
|
||||
<div class="text-right">
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#myModal">
|
||||
{% translate "Search Applications" %}
|
||||
</button>
|
||||
</div>
|
||||
</h1>
|
||||
<div class="container-fluid">
|
||||
<table class="table table-bordered">
|
||||
{% translate "Application Search Results" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#modal-hr-search">
|
||||
{% translate "Search Applications" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "Application ID" %}</th>
|
||||
<th class="text-center">{% translate "Username" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
<th class="text-center">{% translate "Corporation" %}</th>
|
||||
<th>{% translate "Application ID" %}</th>
|
||||
<th>{% translate "Username" %}</th>
|
||||
<th>{% translate "Main Character" %}</th>
|
||||
<th>{% translate "Corporation" %}</th>
|
||||
<th class="text-center">{% translate "Status" %}</th>
|
||||
<th class="text-center">{% translate "Actions" %}</th>
|
||||
<th class="text-end">{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for app in applications %}
|
||||
<tr>
|
||||
<td class="text-center">{{ app.id }}</td>
|
||||
<td class="text-center">{{ app.user }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
<td class="text-center">{{ app.form.corp }}</td>
|
||||
<td>{{ app.id }}</td>
|
||||
<td>{{ app.user }}</td>
|
||||
<td >{{ app.main_character }}</td>
|
||||
<td>{{ app.form.corp }}</td>
|
||||
<td class="text-center">
|
||||
{% if app.approved == None %}
|
||||
<div class="label label-warning">{% translate "Pending" %}</div>
|
||||
{% elif app.approved == True %}
|
||||
<div class="label label-success">{% translate "Approved" %}</div>
|
||||
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||
{% elif app.approved == True %}
|
||||
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||
{% else %}
|
||||
<div class="label label-danger">{% translate "Rejected" %}</div>
|
||||
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-end">
|
||||
<a href="{% url 'hrapplications:view' app.id %}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-eye-open"></span>
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -53,27 +62,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.auth.human_resources %}
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">{% translate "Close" %}</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">{% translate "Application Search" %}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-signin" role="form" action="{% url 'hrapplications:search' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ search_form|bootstrap }}
|
||||
<br>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "hrapplications/partials/modals/search.html" %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,149 +1,179 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% translate "View Application" %}{% endblock page_title %}
|
||||
{% block extra_css %}{% endblock extra_css %}
|
||||
{% block page_title %}
|
||||
{% translate "View Application" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "HR Application Management" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% translate "View Application" %}</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="row">
|
||||
{% if app.approved %}
|
||||
<div class="alert alert-success text-center">{% translate "Approved" %}</div>
|
||||
{% elif app.approved == False %}
|
||||
<div class="alert alert-danger text-center">{% translate "Denied" %}</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "Pending" %}</div>
|
||||
{% endif %}
|
||||
{% if app.reviewer_str %}
|
||||
<div class="alert alert-info text-center">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">{% translate "Applicant" %}</div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "User" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">{{ app.user }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">{% translate "Characters" %}</div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% translate "Name" %}</th>
|
||||
<th class="text-center">{% translate "Corp" %}</th>
|
||||
<th class="text-center">{% translate "Alliance" %}</th>
|
||||
</tr>
|
||||
{% for char in app.characters %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar img-responsive img-circle" src="{{ char.portrait_url_32 }}" alt="{{ 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.alliance_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% for response in responses %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">{{ response.question.title }}</div>
|
||||
<div class="alert">{{ response.answer|linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if buttons %}
|
||||
{% if perms.auth.human_resources %}
|
||||
<div class="row">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">{% translate "Actions" %}</div>
|
||||
<div class="panel-body text-center">
|
||||
{% if app.approved == None %}
|
||||
{% if app.reviewer == user %}
|
||||
{% if perms.hrapplications.approve_application %}
|
||||
<a href="{% url 'hrapplications:approve' app.id %}" class="btn btn-success">{% translate "Approve" %}</a>
|
||||
{% endif %}
|
||||
{% if perms.hrapplications.reject_application %}
|
||||
<a href="{% url 'hrapplications:reject' app.id %}" class="btn btn-danger">{% translate "Reject" %}</a>
|
||||
{% endif %}
|
||||
{% if perms.hrapplications.delete_application %}
|
||||
<a href="{% url 'hrapplications:remove' app.id %}" class="btn btn-danger">{% translate "Delete" %}</a>
|
||||
{% endif %}
|
||||
{% elif not app.reviewer %}
|
||||
<a href="{% url 'hrapplications:mark_in_progress' app.id %}" class="btn btn-warning">{% translate "Mark in Progress" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if perms.hrapplications.add_applicationcomment %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#myModal">{% translate "Comment" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingThree">
|
||||
<h4 class="panel-title">
|
||||
<a class="collapsed" data-toggle="collapse" data-parent="#accordion" href="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||
{% translate 'Comments' %} ({{ comments.count }})
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseThree" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingThree">
|
||||
<div class="panel-body">
|
||||
{% for comment in comments %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="">
|
||||
<div class="panel-title">
|
||||
<div class="pull-right">{{ comment.created }}</div>
|
||||
<div class="pull-left">{% if comment.user.profile.main_character %}{{ comment.user.profile.main_character }}{% else %}{{ comment.user }}{% endif %}</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">{{ comment.text|linebreaks }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div>
|
||||
{% translate "View Application" as page_header %}
|
||||
{% include "framework/header/page-header.html" with title=page_header %}
|
||||
|
||||
<div>
|
||||
{% if app.approved %}
|
||||
<div class="alert alert-success text-center">{% translate "Approved" %}</div>
|
||||
{% elif app.approved == False %}
|
||||
<div class="alert alert-danger text-center">{% translate "Denied" %}</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% translate "Pending" %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if app.reviewer_str %}
|
||||
<div class="alert alert-info text-center">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info">
|
||||
<div class="card-title mb-0">{% translate "Applicant" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% translate "User" %}</th>
|
||||
<th class="text-center">{% translate "Main Character" %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">{{ app.user }}</td>
|
||||
<td class="text-center">{{ app.main_character }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info">
|
||||
<div class="card-title mb-0">{% translate "Characters" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Corporation" %}</th>
|
||||
<th>{% translate "Alliance" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for char in app.characters %}
|
||||
<tr>
|
||||
<td>
|
||||
<img class="ra-avatar img-responsive rounded-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
|
||||
</td>
|
||||
<td>{{ char.character_name }}</td>
|
||||
<td>{{ char.corporation_name }}</td>
|
||||
<td>{{ char.alliance_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for response in responses %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{{ response.question.title }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">{{ response.answer|linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if buttons %}
|
||||
{% if perms.auth.human_resources %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{% translate "Actions" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body text-center">
|
||||
{% if app.approved == None %}
|
||||
{% if app.reviewer == user %}
|
||||
{% if perms.hrapplications.approve_application %}
|
||||
<a href="{% url 'hrapplications:approve' app.id %}" class="btn btn-success">{% translate "Approve" %}</a>
|
||||
{% endif %}
|
||||
{% if perms.hrapplications.reject_application %}
|
||||
<a href="{% url 'hrapplications:reject' app.id %}" class="btn btn-warning">{% translate "Reject" %}</a>
|
||||
{% endif %}
|
||||
{% if perms.hrapplications.delete_application %}
|
||||
<a href="{% url 'hrapplications:remove' app.id %}" class="btn btn-danger">{% translate "Delete" %}</a>
|
||||
{% endif %}
|
||||
{% elif not app.reviewer %}
|
||||
<a href="{% url 'hrapplications:mark_in_progress' app.id %}" class="btn btn-warning">{% translate "Mark in Progress" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if perms.hrapplications.add_applicationcomment %}
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modal-hr-comment">{% translate "Comment" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
<div class="card card-default">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">
|
||||
{% translate 'Comments' %} ({{ comments.count }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
<div class="card card-default">
|
||||
<div class="card-header" role="tab" id="">
|
||||
<div class="card-title mb-0 clearfix">
|
||||
<div class="float-md-end">{{ comment.created }}</div>
|
||||
<div class="float-md-start">{% if comment.user.profile.main_character %}{{ comment.user.profile.main_character }}{% else %}{{ comment.user }}{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">{{ comment.text|linebreaks }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{% translate "No comments" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.hrapplications.add_applicationcomment %}
|
||||
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal fade" id="modal-hr-comment" tabindex="-1" aria-labelledby="modalHrComment" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span><span class="sr-only">{% translate "Close" %}</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="myModalLabel">{% translate "Add Comment" %}</h4>
|
||||
<div class="modal-title fs-5" id="modalHrCommentLabel">
|
||||
{% translate "Add Comment" %}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Close' %}"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="form-signin" role="form" action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ comment_form|bootstrap }}
|
||||
<br>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Add Comment" %}</button>
|
||||
|
||||
{% bootstrap_form comment_form %}
|
||||
|
||||
<div class="form-group mt-3 clearfix">
|
||||
{% translate "Add comment" as button_text %}
|
||||
{% bootstrap_button button_class="btn btn-primary" content=button_text name="addComment" id="addComment" %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
BIN
allianceauth/locale/cs/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/cs/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2776
allianceauth/locale/cs/LC_MESSAGES/django.po
Normal file
2776
allianceauth/locale/cs/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user