Compare commits

...

98 Commits

Author SHA1 Message Date
Ariel Rin
da93940e13 Just an empty Tag Commit, because 2.11.2 bump went wonky 2022-03-29 14:48:39 +10:00
Ariel Rin
f53b43d9dc Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.11.x 2022-03-29 14:47:40 +10:00
Ariel Rin
497a167ca7 Version Bump v2.11.2 2022-03-29 14:46:59 +10:00
Ariel Rin
852c5a3037 Bump Django-ESI to 4.x, inc breaking CCP change in 4.0.1 2022-03-29 14:40:30 +10:00
Ariel Rin
90f6777a7a Version Bump 2.11.1 2022-03-20 14:42:39 +10:00
Ariel Rin
a8d890abaf Merge branch 'improve_task_statistics' into 'master'
Improve task statistics

See merge request allianceauth/allianceauth!1409
2022-03-09 10:04:14 +00:00
Erik Kalkoken
79379b444c Improve task statistics 2022-03-09 10:04:13 +00:00
Ariel Rin
ace1de5c68 Merge branch 'fix-docker-new-redis' into 'master'
Fix docker for new redis

See merge request allianceauth/allianceauth!1406
2022-03-09 10:02:01 +00:00
Kevin McKernan
5d6128e9ea remove collectstatic command from dockerfile 2022-03-01 13:23:49 -07:00
Ariel Rin
131cc5ed0a Version Bump 2.11.0 2022-02-26 17:26:55 +10:00
Ariel Rin
9297bed43f Version Bump 2.10.2 2022-02-26 16:37:20 +10:00
Ariel Rin
b2fddc683a Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.10.x 2022-02-26 16:32:45 +10:00
Ariel Rin
9af634d16a Merge branch 'fix_show_available_groups_for_user_only' into 'master'
Fix: Users can be assigned to groups depite not matching state restrictions

See merge request allianceauth/allianceauth!1402
2022-02-26 05:19:45 +00:00
Erik Kalkoken
a68163caa3 Fix: Users can be assigned to groups depite not matching state restrictions 2022-02-26 05:19:45 +00:00
Ariel Rin
00770fd034 Merge branch 'improve_celery_info_on_dashboard' into 'master'
Improve celery infos on Dashboard

See merge request allianceauth/allianceauth!1384
2022-02-26 05:15:30 +00:00
Erik Kalkoken
01164777ed Improve celery infos on Dashboard 2022-02-26 05:15:30 +00:00
Ariel Rin
00f5e3e1e0 Version Bump 2.10.1 2022-02-21 00:02:12 +10:00
Ariel Rin
8b2527f408 Merge branch 'capsleekxmpp' into 'master'
Cap sleekxmpp to 1.3.2

See merge request allianceauth/allianceauth!1401
2022-02-20 13:44:27 +00:00
Ariel Rin
b7500e4e4e Cap sleekxmpp to 1.3.2 2022-02-20 13:44:27 +00:00
Kevin McKernan
4f4bd0c419 add note to docker README about Apple M1 support 2022-02-20 23:41:12 +10:00
Ariel Rin
8ae4e02012 Merge branch 'docker-bump-version' into 'v2.10.x'
Bump version for Docker deployment to v2.10.x.

See merge request allianceauth/allianceauth!1396
2022-02-02 13:26:33 +00:00
Weyland
cc9a07197d Bump version for Docker deployment to v2.10.x. 2022-02-02 13:30:05 +01:00
Ariel Rin
f18dd1029b Version Bump v2.10.0 2022-01-31 20:58:09 +10:00
Ariel Rin
fd8d43571a Merge branch 'analytics' into 'master'
Analytics - Extra Ignore Path

See merge request allianceauth/allianceauth!1347
2022-01-31 09:23:43 +00:00
Ariel Rin
13e88492f1 Analytics - Extra Ignore Path 2022-01-31 09:23:43 +00:00
Ariel Rin
38df580a56 Merge branch 'analytics_update' into 'master'
Add setting to disable analytics

See merge request allianceauth/allianceauth!1373
2022-01-27 05:14:12 +00:00
Erik Kalkoken
ba39318313 Add setting to disable analytics 2022-01-27 05:14:11 +00:00
Ariel Rin
d8c6035405 Merge branch 'ts3_reserved_groups' into 'master'
Implement reserved group names in Teamspeak3 service module.

See merge request allianceauth/allianceauth!1380
2022-01-27 05:10:22 +00:00
Ariel Rin
2ef3da916b Merge branch 'datatablessavestate' into 'master'
Add DataTables stateSave feature

See merge request allianceauth/allianceauth!1374
2022-01-27 05:05:37 +00:00
Ariel Rin
d32d8b26ce Merge branch 'delete_characters' into 'master'
Fix: Can not update biomassed characters

See merge request allianceauth/allianceauth!1381
2022-01-27 05:02:57 +00:00
Erik Kalkoken
f348b1a34c Fix: Can not update biomassed characters 2022-01-27 05:02:57 +00:00
Ariel Rin
86aaa3edda Merge branch 'fix-grafana-image-2' into 'master'
fix grafana image again, thanks grafana for not tagging your new images properly

See merge request allianceauth/allianceauth!1393
2022-01-27 04:57:40 +00:00
Ariel Rin
26017056c7 Merge branch 'evetime-js-update' into 'master'
Evetime js update

See merge request allianceauth/allianceauth!1395
2022-01-27 04:35:15 +00:00
Peter Pfeufer
e39a3c072b Evetime js update 2022-01-27 04:35:15 +00:00
Kevin McKernan
827291dda4 fix grafana image again, thanks grafana for not tagging your new images properly 2022-01-07 10:48:50 -07:00
Ariel Rin
ea8958ccc3 Version Bump v2.9.4 2021-12-28 21:56:46 +10:00
Ariel Rin
20554df857 Merge branch 'jqueryui' into 'master'
Missing jQuery-UI Images

See merge request allianceauth/allianceauth!1391
2021-12-28 11:55:37 +00:00
Ariel Rin
750f43eaf0 Missing jQuery-UI Images 2021-12-28 11:55:37 +00:00
Ariel Rin
09cf28ec9f Merge branch 'jqueryui' into 'master'
Removing Themeing from jQuery UI

See merge request allianceauth/allianceauth!1386
2021-12-28 11:30:05 +00:00
Ariel Rin
b61746b3cb Removing Themeing from jQuery UI 2021-12-28 11:30:05 +00:00
Ariel Rin
22c22fafeb Merge branch 'py3.11' into 'master'
Python 3.11 Testing

See merge request allianceauth/allianceauth!1388
2021-12-28 11:29:33 +00:00
Ariel Rin
577c4395c4 Python 3.11 Testing 2021-12-28 11:29:33 +00:00
Ariel Rin
d241f476f7 Merge branch 'fix-grafana-image' into 'master'
update grafana image

See merge request allianceauth/allianceauth!1389
2021-12-28 10:43:43 +00:00
Kevin McKernan
5832ed0c30 update grafana image 2021-12-28 10:43:43 +00:00
Ariel Rin
bd9ea225be Rogue comment annoying MRs 2021-12-24 05:02:46 +00:00
Ariel Rin
4a575dd70c Merge branch 'groupmanagement-auto-leave-improvements' into 'master'
Hide "Leave Requests" tab when GROUPMANAGEMENT_AUTO_LEAVE = True

See merge request allianceauth/allianceauth!1378
2021-12-24 04:54:55 +00:00
Ariel Rin
b80ee16a7c Merge branch 'fix_max_notifications_warning' into 'master'
Fix: NOTIFICATIONS_MAX_PER_USER warning when not set

See merge request allianceauth/allianceauth!1383
2021-12-24 04:53:40 +00:00
Ariel Rin
c888371e6c Merge branch 'fix_esi_spec' into 'master'
Add missing ESI operation to eveonline swagger spec file.

See merge request allianceauth/allianceauth!1382
2021-12-24 04:51:49 +00:00
Adarnof
8de2c3bfcb Update name of serverquery IP file changed in TS3 v3.13.0
Changelog indicates old filenames are still accepted, but newly installed servers come with the new file names.
Closes #1298
2021-12-16 22:23:15 -05:00
Adarnof
6688f73565 Use integer teamspeak group IDs when filtering. 2021-12-15 23:54:53 -05:00
ErikKalkoken
7d929cb6e2 Fix: NOTIFICATIONS_MAX_PER_USER warning when not set 2021-12-09 18:12:18 +01:00
Adarnof
72740b9e4d Prevent assignment of reserved groups to AuthTSgroup mappings.
Implemented in TS group updates to prevent their creation / delete once
reserved, and the admin site for when a reserved group name is created
but before the TS group sync occurs.
2021-12-08 23:41:10 -05:00
Adarnof
f7d279fa16 Add missing ESI operation to minimized spec file. My bad. 2021-12-07 23:42:02 -05:00
Ariel Rin
ff7c9c48f3 Merge branch 'v2.9.x' of https://gitlab.com/allianceauth/allianceauth 2021-12-02 02:26:33 +10:00
Adarnof
d11832913d Implement reserved group names in Teamspeak3 service module.
Closes #1302
2021-12-01 00:50:29 -05:00
Ariel Rin
724e0e83f2 Merge branch 'fix-docker-script' into 'v2.9.x'
fix download script

See merge request allianceauth/allianceauth!1379
2021-11-30 11:44:35 +00:00
Kevin McKernan
333f091f1a fix download script 2021-11-29 13:43:32 -07:00
Peter Pfeufer
cfbb0b993a Behavior added to documentation 2021-11-28 19:07:14 +01:00
Peter Pfeufer
582b6754a4 Hide "Leave Requests" tab when GROUPMANAGEMENT_AUTO_LEAVE = True 2021-11-28 18:12:02 +01:00
Ariel Rin
7767c46bf4 Version Bump v2.9.3 2021-11-28 15:37:17 +00:00
Ariel Rin
bf34cef896 Merge branch 'transifex' into 'v2.9.x'
Update from Transifex

See merge request allianceauth/allianceauth!1377
2021-11-28 15:36:14 +00:00
Ariel Rin
c085ec6860 Update from Transifex 2021-11-29 01:34:13 +10:00
Ariel Rin
5f5d0316b2 Merge branch 'transifex' into 'v2.9.x'
Update from Transifex

See merge request allianceauth/allianceauth!1376
2021-11-28 15:25:50 +00:00
Ariel Rin
f9ec64c3ad Update from Transifex 2021-11-28 15:25:50 +00:00
Ariel Rin
0dfd0ad4b0 Django support for Python 3.10 was added to django 3.2.9, missed this in MR !1367 2021-11-28 15:17:53 +00:00
Ariel Rin
e88e11b9ba Merge branch 'jqueryui' into 'v2.9.x'
Add jqueryui Bundles

See merge request allianceauth/allianceauth!1372
2021-11-28 14:50:22 +00:00
Ariel Rin
7a2a79ca7b Merge branch 'groups_blacklist' into 'v2.9.x'
Add blacklist for groups and ignore blacklisted roles in Discord service

See merge request allianceauth/allianceauth!1371
2021-11-28 14:48:49 +00:00
Erik Kalkoken
4c0683c484 Add blacklist for groups and ignore blacklisted roles in Discord service 2021-11-28 14:48:49 +00:00
Ariel Rin
dfe62db8ee add datatables savestate feature 2021-11-27 23:02:33 +10:00
Ariel Rin
025c824fbb Merge branch 'build-docker-image' into 'v2.9.x'
Build docker image in gitlab, add docs for turn key docker setup

See merge request allianceauth/allianceauth!1356
2021-11-27 04:45:21 +00:00
Kevin McKernan
930c5d7c7a Build docker image in gitlab, add docs for turn key docker setup 2021-11-27 04:45:21 +00:00
Ariel Rin
8b8dcc0127 Merge branch 'group-auto-leave-enhancements' into 'v2.9.x'
Group auto leave enhancements

See merge request allianceauth/allianceauth!1369
2021-11-27 03:45:31 +00:00
Peter Pfeufer
4ad8e88bd8 Old reference to AUTO_LEAVE removed 2021-11-27 04:40:23 +01:00
Ariel Rin
89d4640e92 Merge branch 'py310' into 'v2.9.x'
Python 3.10 Support

See merge request allianceauth/allianceauth!1367
2021-11-27 03:35:40 +00:00
Ariel Rin
60b12bad61 Python 3.10 Support 2021-11-27 03:35:40 +00:00
Ariel Rin
2595fa5c51 Merge branch 'gitlab-api-request-error-handling' into 'v2.9.x'
Added error handling when fetching from GitLab API

See merge request allianceauth/allianceauth!1370
2021-11-27 03:31:39 +00:00
Ariel Rin
3e487e5f13 Add jqueryui bundles 2021-11-24 17:56:15 +10:00
Peter Pfeufer
b6d6c68e54 Fix tests
Thanks to @ErikKalkoken for helping here
2021-11-23 02:31:18 +01:00
Peter Pfeufer
49548d6f9f test_can_handle_connection_timeout added 2021-11-23 01:23:34 +01:00
Peter Pfeufer
8faadc23b0 Set logger to warning to not trigger a notification to admins every time 2021-11-23 01:02:08 +01:00
Peter Pfeufer
20da1ebfab request.raise_for_status() re-added 2021-11-22 18:56:33 +01:00
Peter Pfeufer
03305c72c7 Added error handling when fetching from GitLab API 2021-11-22 18:10:02 +01:00
Peter Pfeufer
a636fd1cf0 Add it to the docu 2021-11-21 18:24:04 +01:00
Peter Pfeufer
d8797a8dc6 Prefix the setting so it is clear where it belongs to 2021-11-21 18:18:05 +01:00
Ariel Rin
363e18e15d Merge branch 'more_group_notifications' into 'v2.9.x'
Add option to notify approvers about new group requests

See merge request allianceauth/allianceauth!1363
2021-11-20 01:32:20 +00:00
Erik Kalkoken
982cac8c43 Add option to notify approvers about new group requests 2021-11-20 01:32:20 +00:00
Ariel Rin
fabf64b838 Merge branch 'cherry-pick-f17ebbed' into 'v2.9.x'
Use danger and error message tags to render correctly on admin site.

Closes #1305

See merge request allianceauth/allianceauth!1366
2021-11-20 01:27:26 +00:00
Ariel Rin
e3e6ebe953 Merge branch 'improve_menu_bar' into 'v2.9.x'
Remove seconds from eve clock in menu bar

See merge request allianceauth/allianceauth!1365
2021-11-20 01:26:24 +00:00
Ariel Rin
7ad9b52546 Merge branch 'fix_tox_ini' into 'v2.9.x'
Fix tox environment config

See merge request allianceauth/allianceauth!1364
2021-11-20 01:16:13 +00:00
Adarnof
abb5090d63 Use danger and error message tags to render correctly on admin site.
Closes #1305


(cherry picked from commit f17ebbede6)
2021-11-20 01:14:07 +00:00
Ariel Rin
52ae05d057 Merge branch 'messages' into 'master'
Use danger and error tags to render messages correctly on admin site.

See merge request allianceauth/allianceauth!1362
2021-11-20 01:13:27 +00:00
ErikKalkoken
3cd216d119 Remove seconds from eve clock in menu bar 2021-11-11 21:14:48 +01:00
ErikKalkoken
581edc0a38 Fix environment config in tox.ini 2021-11-11 17:37:27 +01:00
Adarnof
f17ebbede6 Use danger and error message tags to render correctly on admin site.
Closes #1305
2021-11-03 21:02:03 -04:00
Ariel Rin
a19302babc Merge branch 'chunking_updates' into 'master'
Breakout Model updates into separate tasks

See merge request allianceauth/allianceauth!1343
2021-10-19 00:49:39 +00:00
Aaron Kable
18a627b01e Breakout Model updates into separate tasks 2021-10-19 00:49:39 +00:00
Ariel Rin
eddb5480e9 Merge branch 'v2.9.x' into 'master'
Bring up Master to 2.9.0

See merge request allianceauth/allianceauth!1344
2021-10-18 01:58:24 +00:00
Ariel Rin
5b26757662 Merge branch 'features/kick-from-discord-admin' into 'master'
Add override to delete the user from discord itself

See merge request allianceauth/allianceauth!1337
2021-10-17 07:34:22 +00:00
120 changed files with 5006 additions and 1445 deletions

View File

@@ -22,3 +22,7 @@ indent_style = tab
[*.bat] [*.bat]
indent_style = tab indent_style = tab
[{Dockerfile,*.dockerfile}]
indent_style = space
indent_size = 4

2
.gitignore vendored
View File

@@ -38,7 +38,6 @@ htmlcov/
.tox/ .tox/
.coverage .coverage
.cache .cache
nosetests.xml
coverage.xml coverage.xml
# Translations # Translations
@@ -77,3 +76,4 @@ celerybeat-schedule
.flake8 .flake8
.pylintrc .pylintrc
Makefile Makefile
.isort.cfg

View File

@@ -1,8 +1,15 @@
.only-default: &only-default
only:
- master
- branches
- merge_requests
stages: stages:
- pre-commit - pre-commit
- gitlab - gitlab
- test - test
- deploy - deploy
- docker
include: include:
- template: Dependency-Scanning.gitlab-ci.yml - template: Dependency-Scanning.gitlab-ci.yml
@@ -15,6 +22,7 @@ before_script:
- pip install wheel tox - pip install wheel tox
pre-commit-check: pre-commit-check:
<<: *only-default
stage: pre-commit stage: pre-commit
image: python:3.6-buster image: python:3.6-buster
variables: variables:
@@ -39,6 +47,7 @@ dependency_scanning:
- pip install wheel tox - pip install wheel tox
test-3.7-core: test-3.7-core:
<<: *only-default
image: python:3.7-bullseye image: python:3.7-bullseye
script: script:
- tox -e py37-core - tox -e py37-core
@@ -48,6 +57,7 @@ test-3.7-core:
cobertura: coverage.xml cobertura: coverage.xml
test-3.8-core: test-3.8-core:
<<: *only-default
image: python:3.8-bullseye image: python:3.8-bullseye
script: script:
- tox -e py38-core - tox -e py38-core
@@ -57,6 +67,7 @@ test-3.8-core:
cobertura: coverage.xml cobertura: coverage.xml
test-3.9-core: test-3.9-core:
<<: *only-default
image: python:3.9-bullseye image: python:3.9-bullseye
script: script:
- tox -e py39-core - tox -e py39-core
@@ -65,7 +76,29 @@ test-3.9-core:
reports: reports:
cobertura: coverage.xml cobertura: coverage.xml
test-3.10-core:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e py310-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.11-core:
<<: *only-default
image: python:3.11-rc-bullseye
script:
- tox -e py311-core
artifacts:
when: always
reports:
cobertura: coverage.xml
allow_failure: true
test-3.7-all: test-3.7-all:
<<: *only-default
image: python:3.7-bullseye image: python:3.7-bullseye
script: script:
- tox -e py37-all - tox -e py37-all
@@ -75,6 +108,7 @@ test-3.7-all:
cobertura: coverage.xml cobertura: coverage.xml
test-3.8-all: test-3.8-all:
<<: *only-default
image: python:3.8-bullseye image: python:3.8-bullseye
script: script:
- tox -e py38-all - tox -e py38-all
@@ -84,6 +118,7 @@ test-3.8-all:
cobertura: coverage.xml cobertura: coverage.xml
test-3.9-all: test-3.9-all:
<<: *only-default
image: python:3.9-bullseye image: python:3.9-bullseye
script: script:
- tox -e py39-all - tox -e py39-all
@@ -92,9 +127,30 @@ test-3.9-all:
reports: reports:
cobertura: coverage.xml cobertura: coverage.xml
test-3.10-all:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e py310-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.11-all:
<<: *only-default
image: python:3.11-rc-bullseye
script:
- tox -e py311-all
artifacts:
when: always
reports:
cobertura: coverage.xml
allow_failure: true
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.9-bullseye image: python:3.10-bullseye
before_script: before_script:
- pip install twine wheel - pip install twine wheel
@@ -105,3 +161,65 @@ deploy_production:
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
build-image:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-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
CURRENT_TAG=$CI_REGISTRY_IMAGE/auth:$CI_COMMIT_TAG
MINOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1-2)
MAJOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1)
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
rules:
- if: $CI_COMMIT_TAG
build-image-dev:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-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
rules:
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
when: manual
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME != ""'
when: never
build-image-mr:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-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
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
- if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
when: never

View File

@@ -1,7 +1,7 @@
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '2.9.2' __version__ = '2.11.2'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}' NAME = f'{__title__} v{__version__}'

View File

@@ -1,5 +1,6 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from .models import AnalyticsTokens, AnalyticsIdentifier from .models import AnalyticsTokens, AnalyticsIdentifier
from .tasks import send_ga_tracking_web_view from .tasks import send_ga_tracking_web_view
@@ -10,6 +11,8 @@ import re
class AnalyticsMiddleware(MiddlewareMixin): class AnalyticsMiddleware(MiddlewareMixin):
def process_response(self, request, response): def process_response(self, request, response):
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks""" """Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
if getattr(settings, "ANALYTICS_DISABLED", False):
return response
analyticstokens = AnalyticsTokens.objects.all() analyticstokens = AnalyticsTokens.objects.all()
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
try: try:

View File

@@ -1,11 +1,11 @@
# Generated by Django 3.1.13 on 2021-10-15 05:02 # Generated by Django 3.1.13 on 2021-10-15 05:02
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations from django.db import migrations
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor): def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# We can't import the Person model directly as it may be a newer # Add /admin/ and /user_notifications_count/ path to ignore
# version than this migration expects. We use the historical version.
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath') AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*") admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
@@ -17,8 +17,19 @@ def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor): def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# nothing should need to migrate away here? #
return True AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
try:
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*", analyticstokens=token)
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*", analyticstokens=token)
admin.delete()
user_notifications_count.delete()
except ObjectDoesNotExist:
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
pass
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.2.8 on 2021-10-19 01:47
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# Add the /account/activate path to ignore
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
token.ignore_paths.add(account_activate)
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
#
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
try:
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*", analyticstokens=token)
account_activate.delete()
except ObjectDoesNotExist:
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
pass
class Migration(migrations.Migration):
dependencies = [
('analytics', '0005_alter_analyticspath_ignore_path'),
]
operations = [
migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
]

View File

@@ -1,7 +1,8 @@
from allianceauth.analytics.tasks import analytics_event
from celery.signals import task_failure, task_success
import logging 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__) logger = logging.getLogger(__name__)
@@ -11,6 +12,8 @@ def process_failure_signal(
sender, task_id, signal, sender, task_id, signal,
args, kwargs, einfo, **kw): args, kwargs, einfo, **kw):
logger.debug("Celery task_failure signal %s" % sender.__class__.__name__) logger.debug("Celery task_failure signal %s" % sender.__class__.__name__)
if getattr(settings, "ANALYTICS_DISABLED", False):
return
category = sender.__module__ category = sender.__module__
@@ -30,6 +33,8 @@ def process_failure_signal(
@task_success.connect @task_success.connect
def celery_success_signal(sender, result=None, **kw): def celery_success_signal(sender, result=None, **kw):
logger.debug("Celery task_success signal %s" % sender.__class__.__name__) logger.debug("Celery task_success signal %s" % sender.__class__.__name__)
if getattr(settings, "ANALYTICS_DISABLED", False):
return
category = sender.__module__ category = sender.__module__

View File

@@ -21,8 +21,8 @@ if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
# Force sending of analytics data during in a debug/test environemt # Force sending of analytics data during in a debug/test environemt
# Usefull for developers working on this feature. # Usefull for developers working on this feature.
logger.warning( logger.warning(
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! " "You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
"This debug instance will send analytics data!") "This debug instance will send analytics data!")
DEBUG_URL = COLLECTION_URL DEBUG_URL = COLLECTION_URL
ANALYTICS_URL = COLLECTION_URL ANALYTICS_URL = COLLECTION_URL
@@ -40,13 +40,12 @@ def analytics_event(category: str,
Send a Google Analytics Event for each token stored Send a Google Analytics Event for each token stored
Includes check for if its enabled/disabled Includes check for if its enabled/disabled
Parameters Args:
------- `category` (str): Celery Namespace
`category` (str): Celery Namespace `action` (str): Task Name
`action` (str): Task Name `label` (str): Optional, Task Success/Exception
`label` (str): Optional, Task Success/Exception `value` (int): Optional, If bulk, Query size, can be a binary True/False
`value` (int): Optional, If bulk, Query size, can be a binary True/False `event_type` (str): Optional, Celery or Stats only, Default to Celery
`event_type` (str): Optional, Celery or Stats only, Default to Celery
""" """
analyticstokens = AnalyticsTokens.objects.all() analyticstokens = AnalyticsTokens.objects.all()
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
@@ -60,20 +59,21 @@ def analytics_event(category: str,
if allowed is True: if allowed is True:
tracking_id = token.token tracking_id = token.token
send_ga_tracking_celery_event.s(tracking_id=tracking_id, send_ga_tracking_celery_event.s(
client_id=client_id, tracking_id=tracking_id,
category=category, client_id=client_id,
action=action, category=category,
label=label, action=action,
value=value).\ label=label,
apply_async(priority=9) value=value).apply_async(priority=9)
@shared_task() @shared_task()
def analytics_daily_stats(): def analytics_daily_stats():
"""Celery Task: Do not call directly """Celery Task: Do not call directly
Gathers a series of daily statistics and sends analytics events containing them""" Gathers a series of daily statistics and sends analytics events containing them
"""
users = install_stat_users() users = install_stat_users()
tokens = install_stat_tokens() tokens = install_stat_tokens()
addons = install_stat_addons() addons = install_stat_addons()

View File

@@ -0,0 +1,108 @@
from unittest.mock import patch
from urllib.parse import parse_qs
import requests_mock
from django.test import TestCase, override_settings
from allianceauth.analytics.tasks import ANALYTICS_URL
from allianceauth.eveonline.tasks import update_character
from allianceauth.tests.auth_utils import AuthUtils
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.mock()
class TestAnalyticsForViews(TestCase):
@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(TestCase):
@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)

View File

@@ -380,6 +380,7 @@ class UserAdmin(BaseUserAdmin):
'username', 'username',
'character_ownerships__character__character_name' 'character_ownerships__character__character_name'
) )
readonly_fields = ('date_joined', 'last_login')
def _characters(self, obj): def _characters(self, obj):
character_ownerships = list(obj.character_ownerships.all()) character_ownerships = list(obj.character_ownerships.all())
@@ -425,10 +426,19 @@ class UserAdmin(BaseUserAdmin):
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return request.user.has_perm('auth.delete_user') return request.user.has_perm('auth.delete_user')
def get_object(self, *args , **kwargs):
obj = super().get_object(*args , **kwargs)
self.obj = obj # storing current object for use in formfield_for_manytomany
return obj
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form"""
if db_field.name == "groups": if db_field.name == "groups":
kwargs["queryset"] = Group.objects.all().order_by(Lower('name')) groups_qs = Group.objects.filter(authgroup__states__isnull=True)
obj_state = self.obj.profile.state
if obj_state:
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
groups_qs = groups_qs | matching_groups_qs
kwargs["queryset"] = groups_qs.order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)

View File

@@ -3,10 +3,14 @@ from django.core.checks import register, Tags
class AuthenticationConfig(AppConfig): class AuthenticationConfig(AppConfig):
name = 'allianceauth.authentication' name = "allianceauth.authentication"
label = 'authentication' label = "authentication"
def ready(self): def ready(self):
super().ready() from allianceauth.authentication import checks, signals # noqa: F401
from allianceauth.authentication import checks, signals from allianceauth.authentication.task_statistics import (
signals as celery_signals,
)
register(Tags.security)(checks.check_login_scopes_setting) register(Tags.security)(checks.check_login_scopes_setting)
celery_signals.reset_counters()

View File

@@ -0,0 +1,40 @@
from collections import namedtuple
import datetime as dt
from .event_series import EventSeries
"""Global series for counting task events."""
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
retried_tasks = EventSeries("RETRIED_TASKS")
failed_tasks = EventSeries("FAILED_TASKS")
_TaskCounts = namedtuple(
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
)
def dashboard_results(hours: int) -> _TaskCounts:
"""Counts of all task events within the given timeframe."""
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
my_earliest = events.first_event(earliest=earliest)
return [my_earliest] if my_earliest else []
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
earliest_events = list()
succeeded_count = succeeded_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
retried_count = retried_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(retried_tasks, earliest)
failed_count = failed_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(failed_tasks, earliest)
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
failed=failed_count,
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
)

View File

@@ -0,0 +1,95 @@
import datetime as dt
from typing import Optional, List
from redis import Redis
from pytz import utc
from django.core.cache import cache
class EventSeries:
"""API for recording and analysing a series of events."""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis = None) -> None:
self._redis = cache.get_master_client() if not redis else redis
if not isinstance(self._redis, Redis):
raise TypeError(
"This class requires a Redis client, but none was provided "
"and the default Django cache backend is not Redis either."
)
self._key_id = str(key_id)
self.clear()
@property
def _key_counter(self):
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
@property
def _key_sorted_set(self):
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
def add(self, event_time: dt.datetime = None) -> None:
"""Add event.
Args:
- event_time: timestamp of event. Will use current time if not specified.
"""
if not event_time:
event_time = dt.datetime.utcnow()
id = self._redis.incr(self._key_counter)
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
def all(self) -> List[dt.datetime]:
"""List of all known events."""
return [
event[1]
for event in self._redis.zrangebyscore(
self._key_sorted_set,
"-inf",
"+inf",
withscores=True,
score_cast_func=self._cast_scores_to_dt,
)
]
def clear(self) -> None:
"""Clear all events."""
self._redis.delete(self._key_sorted_set)
self._redis.delete(self._key_counter)
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
"""Count of events, can be restricted to given timeframe.
Args:
- earliest: Date of first events to count(inclusive), or -infinite if not specified
- latest: Date of last events to count(inclusive), or +infinite if not specified
"""
min = "-inf" if not earliest else earliest.timestamp()
max = "+inf" if not latest else latest.timestamp()
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
"""Date/Time of first event. Returns `None` if series has no events.
Args:
- earliest: Date of first events to count(inclusive), or any if not specified
"""
min = "-inf" if not earliest else earliest.timestamp()
event = self._redis.zrangebyscore(
self._key_sorted_set,
min,
"+inf",
withscores=True,
start=0,
num=1,
score_cast_func=self._cast_scores_to_dt,
)
if not event:
return None
return event[0][1]
@staticmethod
def _cast_scores_to_dt(score) -> dt.datetime:
return dt.datetime.fromtimestamp(float(score), tz=utc)

View File

@@ -0,0 +1,54 @@
from celery.signals import (
task_failure,
task_internal_error,
task_retry,
task_success,
worker_ready
)
from django.conf import settings
from .counters import failed_tasks, retried_tasks, succeeded_tasks
def reset_counters():
"""Reset all counters for the celery status."""
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
def is_enabled() -> bool:
return not bool(
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
)
@worker_ready.connect
def reset_counters_when_celery_restarted(*args, **kwargs):
if is_enabled():
reset_counters()
@task_success.connect
def record_task_succeeded(*args, **kwargs):
if is_enabled():
succeeded_tasks.add()
@task_retry.connect
def record_task_retried(*args, **kwargs):
if is_enabled():
retried_tasks.add()
@task_failure.connect
def record_task_failed(*args, **kwargs):
if is_enabled():
failed_tasks.add()
@task_internal_error.connect
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()

View File

@@ -0,0 +1,51 @@
import datetime as dt
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.counters import (
dashboard_results,
succeeded_tasks,
retried_tasks,
failed_tasks,
)
class TestDashboardResults(TestCase):
def test_should_return_counts_for_given_timeframe_only(self):
# given
earliest_task = now() - dt.timedelta(minutes=15)
succeeded_tasks.clear()
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
succeeded_tasks.add(earliest_task)
succeeded_tasks.add()
succeeded_tasks.add()
retried_tasks.clear()
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
retried_tasks.add(now() - dt.timedelta(seconds=30))
retried_tasks.add()
failed_tasks.clear()
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
failed_tasks.add()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 3)
self.assertEqual(results.retried, 2)
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 0)
self.assertEqual(results.retried, 0)
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)

View File

@@ -0,0 +1,133 @@
import datetime as dt
from pytz import utc
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import EventSeries
class TestEventSeries(TestCase):
def test_should_add_event(self):
# given
events = EventSeries("dummy")
# when
events.add()
# then
result = events.all()
self.assertEqual(len(result), 1)
self.assertAlmostEqual(result[0], now(), delta=dt.timedelta(seconds=30))
def test_should_add_event_with_specified_time(self):
# given
events = EventSeries("dummy")
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
# when
events.add(my_time)
# then
result = events.all()
self.assertEqual(len(result), 1)
self.assertAlmostEqual(result[0], my_time, delta=dt.timedelta(seconds=30))
def test_should_count_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
result = events.count()
# then
self.assertEqual(result, 2)
def test_should_count_zero(self):
# given
events = EventSeries("dummy")
# when
result = events.count()
# then
self.assertEqual(result, 0)
def test_should_count_events_within_timeframe_1(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc),
latest=dt.datetime(2021, 12, 1, 12, 17, tzinfo=utc),
)
# then
self.assertEqual(result, 2)
def test_should_count_events_within_timeframe_2(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(earliest=dt.datetime(2021, 12, 1, 12, 8))
# then
self.assertEqual(result, 3)
def test_should_count_events_within_timeframe_3(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(latest=dt.datetime(2021, 12, 1, 12, 12))
# then
self.assertEqual(result, 2)
def test_should_clear_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
events.clear()
# then
self.assertEqual(events.count(), 0)
def test_should_return_date_of_first_event(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.first_event()
# then
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
def test_should_return_date_of_first_event_with_range(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.first_event(
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc)
)
# then
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
def test_should_return_all_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
results = events.all()
# then
self.assertEqual(len(results), 2)

View File

@@ -0,0 +1,93 @@
from unittest.mock import patch
from celery.exceptions import Retry
from django.test import TestCase, override_settings
from allianceauth.authentication.task_statistics.counters import (
failed_tasks,
retried_tasks,
succeeded_tasks,
)
from allianceauth.authentication.task_statistics.signals import (
reset_counters,
is_enabled,
)
from allianceauth.eveonline.tasks import update_character
@override_settings(
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
)
class TestTaskSignals(TestCase):
fixtures = ["disable_analytics"]
def test_should_record_successful_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.return_value = None
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 1)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
def test_should_record_retried_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.side_effect = Retry
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 1)
def test_should_record_failed_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.side_effect = RuntimeError
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 1)
def test_should_reset_counters(self):
# given
succeeded_tasks.add()
retried_tasks.add()
failed_tasks.add()
# when
reset_counters()
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
class TestIsEnabled(TestCase):
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
def test_enabled(self):
self.assertTrue(is_enabled())
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=True)
def test_disabled(self):
self.assertFalse(is_enabled())

View File

@@ -1,3 +1,4 @@
from bs4 import BeautifulSoup
from urllib.parse import quote from urllib.parse import quote
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@@ -188,7 +189,7 @@ class TestCaseWithTestData(TestCase):
corporation_id=5432, corporation_id=5432,
corporation_name="Xavier's School for Gifted Youngsters", corporation_name="Xavier's School for Gifted Youngsters",
corporation_ticker='MUTNT', corporation_ticker='MUTNT',
alliance_id = None, alliance_id=None,
faction_id=999, faction_id=999,
faction_name='The X-Men', faction_name='The X-Men',
) )
@@ -206,6 +207,7 @@ class TestCaseWithTestData(TestCase):
cls.user_4.profile.save() cls.user_4.profile.save()
EveFactionInfo.objects.create(faction_id=999, faction_name='The X-Men') EveFactionInfo.objects.create(faction_id=999, faction_name='The X-Men')
def make_generic_search_request(ModelClass: type, search_term: str): def make_generic_search_request(ModelClass: type, search_term: str):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
@@ -218,6 +220,7 @@ def make_generic_search_request(ModelClass: type, search_term: str):
class TestCharacterOwnershipAdmin(TestCaseWithTestData): class TestCharacterOwnershipAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = CharacterOwnershipAdmin( self.modeladmin = CharacterOwnershipAdmin(
@@ -244,6 +247,7 @@ class TestCharacterOwnershipAdmin(TestCaseWithTestData):
class TestOwnershipRecordAdmin(TestCaseWithTestData): class TestOwnershipRecordAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = OwnershipRecordAdmin( self.modeladmin = OwnershipRecordAdmin(
@@ -270,6 +274,7 @@ class TestOwnershipRecordAdmin(TestCaseWithTestData):
class TestStateAdmin(TestCaseWithTestData): class TestStateAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = StateAdmin( self.modeladmin = StateAdmin(
@@ -299,6 +304,7 @@ class TestStateAdmin(TestCaseWithTestData):
class TestUserAdmin(TestCaseWithTestData): class TestUserAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
@@ -344,7 +350,7 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(user_main_organization(self.user_3), expected) self.assertEqual(user_main_organization(self.user_3), expected)
def test_user_main_organization_u4(self): def test_user_main_organization_u4(self):
expected="Xavier's School for Gifted Youngsters<br>The X-Men" expected = "Xavier's School for Gifted Youngsters<br>The X-Men"
self.assertEqual(user_main_organization(self.user_4), expected) self.assertEqual(user_main_organization(self.user_4), expected)
def test_characters_u1(self): def test_characters_u1(self):
@@ -419,7 +425,7 @@ class TestUserAdmin(TestCaseWithTestData):
# actions # actions
@patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True) @patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True, unsafe=True)
@patch(MODULE_PATH + '.update_character') @patch(MODULE_PATH + '.update_character')
def test_action_update_main_character_model( def test_action_update_main_character_model(
self, mock_task, mock_message_user self, mock_task, mock_message_user
@@ -537,6 +543,42 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(response.status_code, expected) self.assertEqual(response.status_code, expected)
class TestUserAdminChangeForm(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.modeladmin = UserAdmin(model=User, admin_site=AdminSite())
def test_should_show_groups_available_to_user_with_blue_state_only(self):
# given
superuser = User.objects.create_superuser("Super")
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(
user,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies"
)
blue_state = State.objects.get(name="Blue")
blue_state.member_characters.add(character)
member_state = AuthUtils.get_member_state()
group_1 = Group.objects.create(name="Group 1")
group_2 = Group.objects.create(name="Group 2")
group_2.authgroup.states.add(blue_state)
group_3 = Group.objects.create(name="Group 3")
group_3.authgroup.states.add(member_state)
self.client.force_login(superuser)
# when
response = self.client.get(f"/admin/authentication/user/{user.pk}/change/")
# then
self.assertEqual(response.status_code, 200)
soup = BeautifulSoup(response.rendered_content, features="html.parser")
groups_select = soup.find("select", {"id": "id_groups"}).find_all('option')
group_ids = {int(option["value"]) for option in groups_select}
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
class TestMakeServicesHooksActions(TestCaseWithTestData): class TestMakeServicesHooksActions(TestCaseWithTestData):
class MyServicesHookTypeA(ServicesHook): class MyServicesHookTypeA(ServicesHook):

View File

@@ -1,6 +1,7 @@
from math import ceil from math import ceil
from unittest.mock import patch from unittest.mock import patch
import requests
import requests_mock import requests_mock
from packaging.version import Version as Pep440Version from packaging.version import Version as Pep440Version
@@ -54,7 +55,6 @@ TEST_VERSION = '2.6.5'
class TestStatusOverviewTag(TestCase): class TestStatusOverviewTag(TestCase):
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length') @patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
@patch(MODULE_PATH + '.admin_status._current_version_summary') @patch(MODULE_PATH + '.admin_status._current_version_summary')
@@ -65,6 +65,7 @@ class TestStatusOverviewTag(TestCase):
mock_current_version_info, mock_current_version_info,
mock_fetch_celery_queue_length mock_fetch_celery_queue_length
): ):
# given
notifications = { notifications = {
'notifications': GITHUB_NOTIFICATION_ISSUES[:5] 'notifications': GITHUB_NOTIFICATION_ISSUES[:5]
} }
@@ -82,22 +83,20 @@ class TestStatusOverviewTag(TestCase):
} }
mock_current_version_info.return_value = version_info mock_current_version_info.return_value = version_info
mock_fetch_celery_queue_length.return_value = 3 mock_fetch_celery_queue_length.return_value = 3
# when
result = status_overview() result = status_overview()
expected = { # then
'notifications': GITHUB_NOTIFICATION_ISSUES[:5], self.assertEqual(result["notifications"], GITHUB_NOTIFICATION_ISSUES[:5])
'latest_major': True, self.assertTrue(result["latest_major"])
'latest_minor': True, self.assertTrue(result["latest_minor"])
'latest_patch': True, self.assertTrue(result["latest_patch"])
'latest_beta': False, self.assertFalse(result["latest_beta"])
'current_version': TEST_VERSION, self.assertEqual(result["current_version"], TEST_VERSION)
'latest_major_version': '2.4.5', self.assertEqual(result["latest_major_version"], '2.4.5')
'latest_minor_version': '2.4.0', self.assertEqual(result["latest_minor_version"], '2.4.0')
'latest_patch_version': '2.4.5', self.assertEqual(result["latest_patch_version"], '2.4.5')
'latest_beta_version': '2.4.4a1', self.assertEqual(result["latest_beta_version"], '2.4.4a1')
'task_queue_length': 3, self.assertEqual(result["task_queue_length"], 3)
}
self.assertEqual(result, expected)
class TestNotifications(TestCase): class TestNotifications(TestCase):
@@ -307,3 +306,25 @@ class TestFetchListFromGitlab(TestCase):
result = _fetch_list_from_gitlab(self.url, max_pages=max_pages) result = _fetch_list_from_gitlab(self.url, max_pages=max_pages)
self.assertEqual(result, GITHUB_TAGS[:4]) self.assertEqual(result, GITHUB_TAGS[:4])
self.assertEqual(requests_mocker.call_count, max_pages) self.assertEqual(requests_mocker.call_count, max_pages)
@requests_mock.mock()
@patch(MODULE_PATH + '.admin_status.logger')
def test_should_not_raise_any_exception_from_github_request_but_log_as_warning(
self, requests_mocker, mock_logger
):
for my_exception in [
requests.exceptions.ConnectionError,
requests.exceptions.HTTPError,
requests.exceptions.URLRequired,
requests.exceptions.TooManyRedirects,
requests.exceptions.ConnectTimeout,
requests.exceptions.Timeout,
]:
requests_mocker.get(self.url, exc=my_exception)
try:
result = _fetch_list_from_gitlab(self.url)
except Exception as ex:
self.fail(f"Unexpected exception raised: {ex}")
self.assertTrue(mock_logger.warning.called)
self.assertListEqual(result, [])

View File

@@ -193,6 +193,8 @@
"columnDefs": [ "columnDefs": [
{ "sortable": false, "targets": [1] }, { "sortable": false, "targets": [1] },
], ],
"stateSave": true,
"stateDuration": 0
}); });
$('#table-members').DataTable({ $('#table-members').DataTable({
"columnDefs": [ "columnDefs": [
@@ -200,6 +202,8 @@
{ "sortable": false, "targets": [0, 2] }, { "sortable": false, "targets": [0, 2] },
], ],
"order": [[ 1, "asc" ]], "order": [[ 1, "asc" ]],
"stateSave": true,
"stateDuration": 0
}); });
$('#table-unregistered').DataTable({ $('#table-unregistered').DataTable({
"columnDefs": [ "columnDefs": [
@@ -207,6 +211,8 @@
{ "sortable": false, "targets": [0, 2] }, { "sortable": false, "targets": [0, 2] },
], ],
"order": [[ 1, "asc" ]], "order": [[ 1, "asc" ]],
"stateSave": true,
"stateDuration": 0
}); });
}); });

View File

@@ -43,6 +43,9 @@
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function(){ $(document).ready(function(){
$('#table-search').DataTable(); $('#table-search').DataTable({
"stateSave": true,
"stateDuration": 0
});
}); });
{% endblock %} {% endblock %}

View File

@@ -1,13 +1,27 @@
from django.db import models import logging
from typing import Union from typing import Union
from .managers import EveCharacterManager, EveCharacterProviderManager from django.core.exceptions import ObjectDoesNotExist
from .managers import EveCorporationManager, EveCorporationProviderManager from django.db import models
from .managers import EveAllianceManager, EveAllianceProviderManager from esi.models import Token
from allianceauth.notifications import notify
from . import providers from . import providers
from .evelinks import eveimageserver from .evelinks import eveimageserver
from .managers import (
EveAllianceManager,
EveAllianceProviderManager,
EveCharacterManager,
EveCharacterProviderManager,
EveCorporationManager,
EveCorporationProviderManager,
)
logger = logging.getLogger(__name__)
_DEFAULT_IMAGE_SIZE = 32 _DEFAULT_IMAGE_SIZE = 32
DOOMHEIM_CORPORATION_ID = 1000001
class EveFactionInfo(models.Model): class EveFactionInfo(models.Model):
@@ -68,13 +82,12 @@ class EveAllianceInfo(models.Model):
for corp_id in alliance.corp_ids: for corp_id in alliance.corp_ids:
if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists(): if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists():
EveCorporationInfo.objects.create_corporation(corp_id) EveCorporationInfo.objects.create_corporation(corp_id)
EveCorporationInfo.objects.filter( EveCorporationInfo.objects.filter(corporation_id__in=alliance.corp_ids).update(
corporation_id__in=alliance.corp_ids).update(alliance=self alliance=self
) )
EveCorporationInfo.objects\ EveCorporationInfo.objects.filter(alliance=self).exclude(
.filter(alliance=self)\ corporation_id__in=alliance.corp_ids
.exclude(corporation_id__in=alliance.corp_ids)\ ).update(alliance=None)
.update(alliance=None)
def update_alliance(self, alliance: providers.Alliance = None): def update_alliance(self, alliance: providers.Alliance = None):
if alliance is None: if alliance is None:
@@ -182,6 +195,7 @@ class EveCorporationInfo(models.Model):
class EveCharacter(models.Model): class EveCharacter(models.Model):
"""Character in Eve Online"""
character_id = models.PositiveIntegerField(unique=True) character_id = models.PositiveIntegerField(unique=True)
character_name = models.CharField(max_length=254, unique=True) character_name = models.CharField(max_length=254, unique=True)
corporation_id = models.PositiveIntegerField() corporation_id = models.PositiveIntegerField()
@@ -198,12 +212,20 @@ class EveCharacter(models.Model):
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['corporation_id',]), models.Index(fields=['corporation_id',]),
models.Index(fields=['alliance_id',]), models.Index(fields=['alliance_id',]),
models.Index(fields=['corporation_name',]), models.Index(fields=['corporation_name',]),
models.Index(fields=['alliance_name',]), models.Index(fields=['alliance_name',]),
models.Index(fields=['faction_id',]), models.Index(fields=['faction_id',]),
] ]
def __str__(self):
return self.character_name
@property
def is_biomassed(self) -> bool:
"""Whether this character is dead or not."""
return self.corporation_id == DOOMHEIM_CORPORATION_ID
@property @property
def alliance(self) -> Union[EveAllianceInfo, None]: def alliance(self) -> Union[EveAllianceInfo, None]:
@@ -249,10 +271,36 @@ class EveCharacter(models.Model):
self.faction_id = character.faction.id self.faction_id = character.faction.id
self.faction_name = character.faction.name self.faction_name = character.faction.name
self.save() self.save()
if self.is_biomassed:
self._remove_tokens_of_biomassed_character()
return self return self
def __str__(self): def _remove_tokens_of_biomassed_character(self) -> None:
return self.character_name """Remove tokens of this biomassed character."""
try:
user = self.character_ownership.user
except ObjectDoesNotExist:
return
tokens_to_delete = Token.objects.filter(character_id=self.character_id)
tokens_count = tokens_to_delete.count()
if not tokens_count:
return
tokens_to_delete.delete()
logger.info(
"%d tokens from user %s for biomassed character %s [id:%s] deleted.",
tokens_count,
user,
self,
self.character_id,
)
notify(
user=user,
title=f"Character {self} biomassed",
message=(
f"Your former character {self} has been biomassed "
"and has been removed from the list of your alts."
)
)
@staticmethod @staticmethod
def generic_portrait_url( def generic_portrait_url(
@@ -336,7 +384,6 @@ class EveCharacter(models.Model):
"""image URL for alliance of this character or empty string""" """image URL for alliance of this character or empty string"""
return self.alliance_logo_url(256) return self.alliance_logo_url(256)
def faction_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str: def faction_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str:
"""image URL for alliance of this character or empty string""" """image URL for alliance of this character or empty string"""
if self.faction_id: if self.faction_id:

View File

@@ -13,17 +13,18 @@ from allianceauth import __version__
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname( SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
os.path.abspath(__file__)), 'swagger.json' os.path.abspath(__file__)), 'swagger.json'
) )
"""
Swagger spec operations:
get_alliances_alliance_id # for the love of Bob please add operations you use here. I'm tired of breaking undocumented things.
get_alliances_alliance_id_corporations ESI_OPERATIONS=[
get_corporations_corporation_id 'get_alliances_alliance_id',
get_characters_character_id 'get_alliances_alliance_id_corporations',
get_universe_types_type_id 'get_corporations_corporation_id',
post_character_affiliation 'get_characters_character_id',
get_universe_factions 'post_characters_affiliation',
""" 'get_universe_types_type_id',
'get_universe_factions',
'post_universe_names',
]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -169,7 +170,7 @@ class EveProvider:
""" """
:return: an ItemType object for the given ID :return: an ItemType object for the given ID
""" """
raise NotImplemented() raise NotImplementedError()
class EveSwaggerProvider(EveProvider): class EveSwaggerProvider(EveProvider):
@@ -206,7 +207,8 @@ class EveSwaggerProvider(EveProvider):
def __str__(self): def __str__(self):
return 'esi' return 'esi'
def get_alliance(self, alliance_id): def get_alliance(self, alliance_id: int) -> Alliance:
"""Fetch alliance from ESI."""
try: try:
data = self.client.Alliance.get_alliances_alliance_id(alliance_id=alliance_id).result() data = self.client.Alliance.get_alliances_alliance_id(alliance_id=alliance_id).result()
corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result() corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result()
@@ -222,7 +224,8 @@ class EveSwaggerProvider(EveProvider):
except HTTPNotFound: except HTTPNotFound:
raise ObjectNotFound(alliance_id, 'alliance') raise ObjectNotFound(alliance_id, 'alliance')
def get_corp(self, corp_id): def get_corp(self, corp_id: int) -> Corporation:
"""Fetch corporation from ESI."""
try: try:
data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result() data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result()
model = Corporation( model = Corporation(
@@ -238,29 +241,43 @@ class EveSwaggerProvider(EveProvider):
except HTTPNotFound: except HTTPNotFound:
raise ObjectNotFound(corp_id, 'corporation') raise ObjectNotFound(corp_id, 'corporation')
def get_character(self, character_id): def get_character(self, character_id: int) -> Character:
"""Fetch character from ESI."""
try: try:
data = self.client.Character.get_characters_character_id(character_id=character_id).result() character_name = self._fetch_character_name(character_id)
affiliation = self.client.Character.post_characters_affiliation(characters=[character_id]).result()[0] affiliation = self.client.Character.post_characters_affiliation(characters=[character_id]).result()[0]
model = Character( model = Character(
id=character_id, id=character_id,
name=data['name'], name=character_name,
corp_id=affiliation['corporation_id'], corp_id=affiliation['corporation_id'],
alliance_id=affiliation['alliance_id'] if 'alliance_id' in affiliation else None, alliance_id=affiliation['alliance_id'] if 'alliance_id' in affiliation else None,
faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None, faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None,
) )
return model return model
except (HTTPNotFound, HTTPUnprocessableEntity): except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound):
raise ObjectNotFound(character_id, 'character') raise ObjectNotFound(character_id, 'character')
def _fetch_character_name(self, character_id: int) -> str:
"""Fetch character name from ESI."""
data = self.client.Universe.post_universe_names(ids=[character_id]).result()
character = data.pop() if data else None
if (
not character
or character["category"] != "character"
or character["id"] != character_id
):
raise ObjectNotFound(character_id, 'character')
return character["name"]
def get_all_factions(self): def get_all_factions(self):
"""Fetch all factions from ESI."""
if not self._faction_list: if not self._faction_list:
self._faction_list = self.client.Universe.get_universe_factions().result() self._faction_list = self.client.Universe.get_universe_factions().result()
return self._faction_list return self._faction_list
def get_faction(self, faction_id): def get_faction(self, faction_id: int):
faction_id=int(faction_id) """Fetch faction from ESI."""
faction_id = int(faction_id)
try: try:
if not self._faction_list: if not self._faction_list:
_ = self.get_all_factions() _ = self.get_all_factions()
@@ -272,7 +289,8 @@ class EveSwaggerProvider(EveProvider):
except (HTTPNotFound, HTTPUnprocessableEntity, KeyError): except (HTTPNotFound, HTTPUnprocessableEntity, KeyError):
raise ObjectNotFound(faction_id, 'faction') raise ObjectNotFound(faction_id, 'faction')
def get_itemtype(self, type_id): def get_itemtype(self, type_id: int) -> ItemType:
"""Fetch inventory item from ESI."""
try: try:
data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result() data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result()
return ItemType(id=type_id, name=data['name']) return ItemType(id=type_id, name=data['name'])

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,11 @@
import logging import logging
from celery import shared_task from celery import shared_task
from .models import EveAllianceInfo
from .models import EveCharacter
from .models import EveCorporationInfo
from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo
from . import providers from . import providers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TASK_PRIORITY = 7 TASK_PRIORITY = 7
@@ -32,8 +31,8 @@ def update_alliance(alliance_id):
@shared_task @shared_task
def update_character(character_id): def update_character(character_id: int) -> None:
"""Update given character from ESI""" """Update given character from ESI."""
EveCharacter.objects.update_character(character_id) EveCharacter.objects.update_character(character_id)
@@ -65,17 +64,17 @@ def update_character_chunk(character_ids_chunk: list):
.post_characters_affiliation(characters=character_ids_chunk).result() .post_characters_affiliation(characters=character_ids_chunk).result()
character_names = providers.provider.client.Universe\ character_names = providers.provider.client.Universe\
.post_universe_names(ids=character_ids_chunk).result() .post_universe_names(ids=character_ids_chunk).result()
except: except OSError:
logger.info("Failed to bulk update characters. Attempting single updates") logger.info("Failed to bulk update characters. Attempting single updates")
for character_id in character_ids_chunk: for character_id in character_ids_chunk:
update_character.apply_async( update_character.apply_async(
args=[character_id], priority=TASK_PRIORITY args=[character_id], priority=TASK_PRIORITY
) )
return return
affiliations = { affiliations = {
affiliation.get('character_id'): affiliation affiliation.get('character_id'): affiliation
for affiliation in affiliations_raw for affiliation in affiliations_raw
} }
# add character names to affiliations # add character names to affiliations
for character in character_names: for character in character_names:
@@ -108,5 +107,5 @@ def update_character_chunk(character_ids_chunk: list):
if corp_changed or alliance_changed or name_changed: if corp_changed or alliance_changed or name_changed:
update_character.apply_async( update_character.apply_async(
args=[character.get('character_id')], priority=TASK_PRIORITY args=[character.get('character_id')], priority=TASK_PRIORITY
) )

View File

@@ -0,0 +1,168 @@
from bravado.exception import HTTPNotFound
class BravadoResponseStub:
"""Stub for IncomingResponse in bravado, e.g. for HTTPError exceptions"""
def __init__(
self, status_code, reason="", text="", headers=None, raw_bytes=None
) -> None:
self.reason = reason
self.status_code = status_code
self.text = text
self.headers = headers if headers else dict()
self.raw_bytes = raw_bytes
def __str__(self):
return f"{self.status_code} {self.reason}"
class BravadoOperationStub:
"""Stub to simulate the operation object return from bravado via django-esi"""
class RequestConfig:
def __init__(self, also_return_response):
self.also_return_response = also_return_response
class ResponseStub:
def __init__(self, headers):
self.headers = headers
def __init__(self, data, headers: dict = None, also_return_response: bool = False):
self._data = data
self._headers = headers if headers else {"x-pages": 1}
self.request_config = BravadoOperationStub.RequestConfig(also_return_response)
def result(self, **kwargs):
if self.request_config.also_return_response:
return [self._data, self.ResponseStub(self._headers)]
else:
return self._data
def results(self, **kwargs):
return self.result(**kwargs)
class EsiClientStub:
"""Stub for an ESI client."""
class Alliance:
@staticmethod
def get_alliances_alliance_id(alliance_id):
data = {
3001: {
"name": "Wayne Enterprises",
"ticker": "WYE",
"executor_corporation_id": 2001
}
}
try:
return BravadoOperationStub(data[int(alliance_id)])
except KeyError:
response = BravadoResponseStub(
404, f"Alliance with ID {alliance_id} not found"
)
raise HTTPNotFound(response)
@staticmethod
def get_alliances_alliance_id_corporations(alliance_id):
data = [2001, 2002, 2003]
return BravadoOperationStub(data)
class Character:
@staticmethod
def get_characters_character_id(character_id):
data = {
1001: {
"corporation_id": 2001,
"name": "Bruce Wayne",
},
1002: {
"corporation_id": 2001,
"name": "Peter Parker",
},
1011: {
"corporation_id": 2011,
"name": "Lex Luthor",
}
}
try:
return BravadoOperationStub(data[int(character_id)])
except KeyError:
response = BravadoResponseStub(
404, f"Character with ID {character_id} not found"
)
raise HTTPNotFound(response)
@staticmethod
def post_characters_affiliation(characters: list):
data = [
{'character_id': 1001, 'corporation_id': 2001, 'alliance_id': 3001},
{'character_id': 1002, 'corporation_id': 2001, 'alliance_id': 3001},
{'character_id': 1011, 'corporation_id': 2011},
{'character_id': 1666, 'corporation_id': 1000001},
]
return BravadoOperationStub(
[x for x in data if x['character_id'] in characters]
)
class Corporation:
@staticmethod
def get_corporations_corporation_id(corporation_id):
data = {
2001: {
"ceo_id": 1091,
"member_count": 10,
"name": "Wayne Technologies",
"ticker": "WTE",
"alliance_id": 3001
},
2002: {
"ceo_id": 1092,
"member_count": 10,
"name": "Wayne Food",
"ticker": "WFO",
"alliance_id": 3001
},
2003: {
"ceo_id": 1093,
"member_count": 10,
"name": "Wayne Energy",
"ticker": "WEG",
"alliance_id": 3001
},
2011: {
"ceo_id": 1,
"member_count": 3,
"name": "LexCorp",
"ticker": "LC",
},
1000001: {
"ceo_id": 3000001,
"creator_id": 1,
"description": "The internal corporation used for characters in graveyard.",
"member_count": 6329026,
"name": "Doomheim",
"ticker": "666",
}
}
try:
return BravadoOperationStub(data[int(corporation_id)])
except KeyError:
response = BravadoResponseStub(
404, f"Corporation with ID {corporation_id} not found"
)
raise HTTPNotFound(response)
class Universe:
@staticmethod
def post_universe_names(ids: list):
data = [
{"category": "character", "id": 1001, "name": "Bruce Wayne"},
{"category": "character", "id": 1002, "name": "Peter Parker"},
{"category": "character", "id": 1011, "name": "Lex Luthor"},
{"category": "character", "id": 1666, "name": "Hal Jordan"},
{"category": "corporation", "id": 2001, "name": "Wayne Technologies"},
{"category": "corporation","id": 2002, "name": "Wayne Food"},
{"category": "corporation","id": 1000001, "name": "Doomheim"},
]
return BravadoOperationStub([x for x in data if x['id'] in ids])

View File

@@ -1,12 +1,15 @@
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase from django.test import TestCase
from esi.models import Token
from allianceauth.tests.auth_utils import AuthUtils
from ..models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
)
from ..providers import Alliance, Corporation, Character
from ..evelinks import eveimageserver from ..evelinks import eveimageserver
from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo
from ..providers import Alliance, Character, Corporation
from .esi_client_stub import EsiClientStub
class EveCharacterTestCase(TestCase): class EveCharacterTestCase(TestCase):
@@ -402,8 +405,8 @@ class EveAllianceTestCase(TestCase):
my_alliance.save() my_alliance.save()
my_alliance.populate_alliance() my_alliance.populate_alliance()
for corporation in EveCorporationInfo.objects\ for corporation in (
.filter(corporation_id__in=[2001, 2002] EveCorporationInfo.objects.filter(corporation_id__in=[2001, 2002])
): ):
self.assertEqual(corporation.alliance, my_alliance) self.assertEqual(corporation.alliance, my_alliance)
@@ -587,3 +590,98 @@ class EveCorporationTestCase(TestCase):
self.my_corp.logo_url_256, self.my_corp.logo_url_256,
'https://images.evetech.net/corporations/2001/logo?size=256' 'https://images.evetech.net/corporations/2001/logo?size=256'
) )
@patch('allianceauth.eveonline.providers.esi_client_factory')
@patch("allianceauth.eveonline.models.notify")
class TestCharacterUpdate(TestCase):
def test_should_update_normal_character(self, mock_notify, mock_esi_client_factory):
# given
mock_esi_client_factory.return_value = EsiClientStub()
my_character = EveCharacter.objects.create(
character_id=1001,
character_name="not my name",
corporation_id=2002,
corporation_name="Wayne Food",
corporation_ticker="WYF",
alliance_id=None
)
# when
my_character.update_character()
# then
my_character.refresh_from_db()
self.assertEqual(my_character.character_name, "Bruce Wayne")
self.assertEqual(my_character.corporation_id, 2001)
self.assertEqual(my_character.corporation_name, "Wayne Technologies")
self.assertEqual(my_character.corporation_ticker, "WTE")
self.assertEqual(my_character.alliance_id, 3001)
self.assertEqual(my_character.alliance_name, "Wayne Enterprises")
self.assertEqual(my_character.alliance_ticker, "WYE")
self.assertFalse(mock_notify.called)
def test_should_update_dead_character_with_owner(
self, mock_notify, mock_esi_client_factory
):
# given
mock_esi_client_factory.return_value = EsiClientStub()
character_1666 = EveCharacter.objects.create(
character_id=1666,
character_name="Hal Jordan",
corporation_id=2002,
corporation_name="Wayne Food",
corporation_ticker="WYF",
alliance_id=None
)
user = AuthUtils.create_user("Bruce Wayne")
token_1666 = Token.objects.create(
user=user,
character_id=character_1666.character_id,
character_name=character_1666.character_name,
character_owner_hash="ABC123-1666",
)
character_1001 = EveCharacter.objects.create(
character_id=1001,
character_name="Bruce Wayne",
corporation_id=2001,
corporation_name="Wayne Technologies",
corporation_ticker="WYT",
alliance_id=None
)
token_1001 = Token.objects.create(
user=user,
character_id=character_1001.character_id,
character_name=character_1001.character_name,
character_owner_hash="ABC123-1001",
)
# when
character_1666.update_character()
# then
character_1666.refresh_from_db()
self.assertTrue(character_1666.is_biomassed)
self.assertNotIn(token_1666, user.token_set.all())
self.assertIn(token_1001, user.token_set.all())
with self.assertRaises(ObjectDoesNotExist):
self.assertTrue(character_1666.character_ownership)
user.profile.refresh_from_db()
self.assertIsNone(user.profile.main_character)
self.assertTrue(mock_notify.called)
def test_should_handle_dead_character_without_owner(
self, mock_notify, mock_esi_client_factory
):
# given
mock_esi_client_factory.return_value = EsiClientStub()
character_1666 = EveCharacter.objects.create(
character_id=1666,
character_name="Hal Jordan",
corporation_id=1011,
corporation_name="LexCorp",
corporation_ticker='LC',
alliance_id=None
)
# when
character_1666.update_character()
# then
character_1666.refresh_from_db()
self.assertTrue(character_1666.is_biomassed)
self.assertFalse(mock_notify.called)

View File

@@ -7,6 +7,7 @@ from jsonschema.exceptions import RefResolutionError
from django.test import TestCase from django.test import TestCase
from . import set_logger from . import set_logger
from .esi_client_stub import EsiClientStub
from ..providers import ( from ..providers import (
ObjectNotFound, ObjectNotFound,
Entity, Entity,
@@ -632,13 +633,7 @@ class TestEveSwaggerProvider(TestCase):
@patch(MODULE_PATH + '.esi_client_factory') @patch(MODULE_PATH + '.esi_client_factory')
def test_get_character(self, mock_esi_client_factory): def test_get_character(self, mock_esi_client_factory):
mock_esi_client_factory.return_value \ mock_esi_client_factory.return_value = EsiClientStub()
.Character.get_characters_character_id \
= TestEveSwaggerProvider.esi_get_characters_character_id
mock_esi_client_factory.return_value \
.Character.post_characters_affiliation \
= TestEveSwaggerProvider.esi_post_characters_affiliation
my_provider = EveSwaggerProvider() my_provider = EveSwaggerProvider()
# character with alliance # character with alliance
@@ -649,8 +644,8 @@ class TestEveSwaggerProvider(TestCase):
self.assertEqual(my_character.alliance_id, 3001) self.assertEqual(my_character.alliance_id, 3001)
# character wo/ alliance # character wo/ alliance
my_character = my_provider.get_character(1002) my_character = my_provider.get_character(1011)
self.assertEqual(my_character.id, 1002) self.assertEqual(my_character.id, 1011)
self.assertEqual(my_character.alliance_id, None) self.assertEqual(my_character.alliance_id, None)
# character not found # character not found

View File

@@ -1,245 +1,271 @@
from unittest.mock import patch, Mock from unittest.mock import patch
from django.test import TestCase from django.test import TestCase, TransactionTestCase, override_settings
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo
from ..tasks import ( from ..tasks import (
run_model_update,
update_alliance, update_alliance,
update_corp,
update_character, update_character,
run_model_update update_character_chunk,
update_corp,
) )
from .esi_client_stub import EsiClientStub
class TestTasks(TestCase): @patch('allianceauth.eveonline.providers.esi_client_factory')
class TestUpdateTasks(TestCase):
@patch('allianceauth.eveonline.tasks.EveCorporationInfo') def test_should_update_alliance(self, mock_esi_client_factory):
def test_update_corp(self, mock_EveCorporationInfo): # given
update_corp(42) mock_esi_client_factory.return_value = EsiClientStub()
self.assertEqual( my_alliance = EveAllianceInfo.objects.create(
mock_EveCorporationInfo.objects.update_corporation.call_count, 1 alliance_id=3001,
) alliance_name="Wayne Enterprises",
self.assertEqual( alliance_ticker="WYE",
mock_EveCorporationInfo.objects.update_corporation.call_args[0][0], 42 executor_corp_id=2003
) )
# when
update_alliance(my_alliance.alliance_id)
# then
my_alliance.refresh_from_db()
self.assertEqual(my_alliance.executor_corp_id, 2001)
@patch('allianceauth.eveonline.tasks.EveAllianceInfo') def test_should_update_character(self, mock_esi_client_factory):
def test_update_alliance(self, mock_EveAllianceInfo): # given
update_alliance(42) mock_esi_client_factory.return_value = EsiClientStub()
self.assertEqual( my_character = EveCharacter.objects.create(
mock_EveAllianceInfo.objects.update_alliance.call_args[0][0], 42 character_id=1001,
) character_name="Bruce Wayne",
self.assertEqual( corporation_id=2002,
mock_EveAllianceInfo.objects corporation_name="Wayne Food",
.update_alliance.return_value.populate_alliance.call_count, 1 corporation_ticker="WYF",
alliance_id=None
) )
# when
update_character(my_character.character_id)
# then
my_character.refresh_from_db()
self.assertEqual(my_character.corporation_id, 2001)
@patch('allianceauth.eveonline.tasks.EveCharacter') def test_should_update_corp(self, mock_esi_client_factory):
def test_update_character(self, mock_EveCharacter): # given
update_character(42) mock_esi_client_factory.return_value = EsiClientStub()
self.assertEqual( EveAllianceInfo.objects.create(
mock_EveCharacter.objects.update_character.call_count, 1 alliance_id=3001,
alliance_name="Wayne Enterprises",
alliance_ticker="WYE",
executor_corp_id=2003
) )
self.assertEqual( my_corporation = EveCorporationInfo.objects.create(
mock_EveCharacter.objects.update_character.call_args[0][0], 42 corporation_id=2003,
corporation_name="Wayne Food",
corporation_ticker="WFO",
member_count=1,
alliance=None,
ceo_id=1999
) )
# when
update_corp(my_corporation.corporation_id)
# then
my_corporation.refresh_from_db()
self.assertEqual(my_corporation.alliance.alliance_id, 3001)
# @patch('allianceauth.eveonline.tasks.EveCharacter')
# def test_update_character(self, mock_EveCharacter):
# update_character(42)
# self.assertEqual(
# mock_EveCharacter.objects.update_character.call_count, 1
# )
# self.assertEqual(
# mock_EveCharacter.objects.update_character.call_args[0][0], 42
# )
@patch('allianceauth.eveonline.tasks.update_character') @override_settings(CELERY_ALWAYS_EAGER=True)
@patch('allianceauth.eveonline.tasks.update_alliance') @patch('allianceauth.eveonline.providers.esi_client_factory')
@patch('allianceauth.eveonline.tasks.update_corp') @patch('allianceauth.eveonline.tasks.providers')
@patch('allianceauth.eveonline.providers.provider')
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2) @patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
class TestRunModelUpdate(TestCase): class TestRunModelUpdate(TransactionTestCase):
def test_should_run_updates(self, mock_providers, mock_esi_client_factory):
@classmethod # given
def setUpClass(cls): mock_providers.provider.client = EsiClientStub()
super().setUpClass() mock_esi_client_factory.return_value = EsiClientStub()
EveCorporationInfo.objects.all().delete()
EveAllianceInfo.objects.all().delete()
EveCharacter.objects.all().delete()
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2345, corporation_id=2001,
corporation_name='corp.name', corporation_name="Wayne Technologies",
corporation_ticker='c.c.t', corporation_ticker="WTE",
member_count=10, member_count=10,
alliance=None, alliance=None,
) )
EveAllianceInfo.objects.create( alliance_3001 = EveAllianceInfo.objects.create(
alliance_id=3456, alliance_id=3001,
alliance_name='alliance.name', alliance_name="Wayne Enterprises",
alliance_ticker='a.t', alliance_ticker="WYE",
executor_corp_id=5, executor_corp_id=2003
) )
EveCharacter.objects.create( corporation_2003 = EveCorporationInfo.objects.create(
character_id=1, corporation_id=2003,
character_name='character.name1', corporation_name="Wayne Energy",
corporation_id=2345, corporation_ticker="WEG",
corporation_name='character.corp.name', member_count=99,
corporation_ticker='c.c.t', # max 5 chars alliance=None,
)
character_1001 = EveCharacter.objects.create(
character_id=1001,
character_name="Bruce Wayne",
corporation_id=2002,
corporation_name="Wayne Food",
corporation_ticker="WYF",
alliance_id=None alliance_id=None
) )
EveCharacter.objects.create( # when
character_id=2,
character_name='character.name2',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
EveCharacter.objects.create(
character_id=3,
character_name='character.name3',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
EveCharacter.objects.create(
character_id=4,
character_name='character.name4',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
"""
EveCharacter.objects.create(
character_id=5,
character_name='character.name5',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
"""
def setUp(self):
self.affiliations = [
{'character_id': 1, 'corporation_id': 5},
{'character_id': 2, 'corporation_id': 9876, 'alliance_id': 3456},
{'character_id': 3, 'corporation_id': 9876, 'alliance_id': 7456},
{'character_id': 4, 'corporation_id': 9876, 'alliance_id': 3456}
]
self.names = [
{'id': 1, 'name': 'character.name1'},
{'id': 2, 'name': 'character.name2'},
{'id': 3, 'name': 'character.name3'},
{'id': 4, 'name': 'character.name4_new'}
]
def test_normal_run(
self,
mock_provider,
mock_update_corp,
mock_update_alliance,
mock_update_character,
):
def get_affiliations(characters: list):
response = [x for x in self.affiliations if x['character_id'] in characters]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
def get_names(ids: list):
response = [x for x in self.names if x['id'] in ids]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
mock_provider.client.Character.post_characters_affiliation.side_effect \
= get_affiliations
mock_provider.client.Universe.post_universe_names.side_effect = get_names
run_model_update() run_model_update()
# then
character_1001.refresh_from_db()
self.assertEqual( self.assertEqual(
mock_provider.client.Character.post_characters_affiliation.call_count, 2 character_1001.corporation_id, 2001 # char has new corp
) )
corporation_2003.refresh_from_db()
self.assertEqual( self.assertEqual(
mock_provider.client.Universe.post_universe_names.call_count, 2 corporation_2003.alliance.alliance_id, 3001 # corp has new alliance
)
alliance_3001.refresh_from_db()
self.assertEqual(
alliance_3001.executor_corp_id, 2001 # alliance has been updated
) )
# character 1 has changed corp
# character 2 no change @override_settings(CELERY_ALWAYS_EAGER=True)
# character 3 has changed alliance @patch('allianceauth.eveonline.tasks.update_character', wraps=update_character)
# character 4 has changed name @patch('allianceauth.eveonline.providers.esi_client_factory')
self.assertEqual(mock_update_corp.apply_async.call_count, 1) @patch('allianceauth.eveonline.tasks.providers')
self.assertEqual( @patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
int(mock_update_corp.apply_async.call_args[1]['args'][0]), 2345 class TestUpdateCharacterChunk(TestCase):
) @staticmethod
self.assertEqual(mock_update_alliance.apply_async.call_count, 1) def _updated_character_ids(spy_update_character) -> set:
self.assertEqual( """Character IDs passed to update_character task for update."""
int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456 return {
) x[1]["args"][0] for x in spy_update_character.apply_async.call_args_list
characters_updated = {
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
} }
excepted = {1, 3, 4}
self.assertSetEqual(characters_updated, excepted)
def test_ignore_character_not_in_affiliations( def test_should_update_corp_change(
self, self, mock_providers, mock_esi_client_factory, spy_update_character
mock_provider,
mock_update_corp,
mock_update_alliance,
mock_update_character,
): ):
def get_affiliations(characters: list): # given
response = [x for x in self.affiliations if x['character_id'] in characters] mock_providers.provider.client = EsiClientStub()
mock_operator = Mock(**{'result.return_value': response}) mock_esi_client_factory.return_value = EsiClientStub()
return mock_operator character_1001 = EveCharacter.objects.create(
character_id=1001,
character_name="Bruce Wayne",
corporation_id=2003,
corporation_name="Wayne Energy",
corporation_ticker="WEG",
alliance_id=3001,
alliance_name="Wayne Enterprises",
alliance_ticker="WYE",
)
character_1002 = EveCharacter.objects.create(
character_id=1002,
character_name="Peter Parker",
corporation_id=2001,
corporation_name="Wayne Technologies",
corporation_ticker="WTE",
alliance_id=3001,
alliance_name="Wayne Enterprises",
alliance_ticker="WYE",
)
# when
update_character_chunk([
character_1001.character_id, character_1002.character_id
])
# then
character_1001.refresh_from_db()
self.assertEqual(character_1001.corporation_id, 2001)
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
def get_names(ids: list): def test_should_update_name_change(
response = [x for x in self.names if x['id'] in ids] self, mock_providers, mock_esi_client_factory, spy_update_character
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
del self.affiliations[0]
mock_provider.client.Character.post_characters_affiliation.side_effect \
= get_affiliations
mock_provider.client.Universe.post_universe_names.side_effect = get_names
run_model_update()
characters_updated = {
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
}
excepted = {3, 4}
self.assertSetEqual(characters_updated, excepted)
def test_ignore_character_not_in_names(
self,
mock_provider,
mock_update_corp,
mock_update_alliance,
mock_update_character,
): ):
def get_affiliations(characters: list): # given
response = [x for x in self.affiliations if x['character_id'] in characters] mock_providers.provider.client = EsiClientStub()
mock_operator = Mock(**{'result.return_value': response}) mock_esi_client_factory.return_value = EsiClientStub()
return mock_operator character_1001 = EveCharacter.objects.create(
character_id=1001,
character_name="Batman",
corporation_id=2001,
corporation_name="Wayne Technologies",
corporation_ticker="WTE",
alliance_id=3001,
alliance_name="Wayne Technologies",
alliance_ticker="WYT",
)
# when
update_character_chunk([character_1001.character_id])
# then
character_1001.refresh_from_db()
self.assertEqual(character_1001.character_name, "Bruce Wayne")
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
def get_names(ids: list): def test_should_update_alliance_change(
response = [x for x in self.names if x['id'] in ids] self, mock_providers, mock_esi_client_factory, spy_update_character
mock_operator = Mock(**{'result.return_value': response}) ):
return mock_operator # given
mock_providers.provider.client = EsiClientStub()
mock_esi_client_factory.return_value = EsiClientStub()
character_1001 = EveCharacter.objects.create(
character_id=1001,
character_name="Bruce Wayne",
corporation_id=2001,
corporation_name="Wayne Technologies",
corporation_ticker="WTE",
alliance_id=None,
)
# when
update_character_chunk([character_1001.character_id])
# then
character_1001.refresh_from_db()
self.assertEqual(character_1001.alliance_id, 3001)
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})
del self.names[3] def test_should_not_update_when_not_changed(
self, mock_providers, mock_esi_client_factory, spy_update_character
):
# given
mock_providers.provider.client = EsiClientStub()
mock_esi_client_factory.return_value = EsiClientStub()
character_1001 = EveCharacter.objects.create(
character_id=1001,
character_name="Bruce Wayne",
corporation_id=2001,
corporation_name="Wayne Technologies",
corporation_ticker="WTE",
alliance_id=3001,
alliance_name="Wayne Technologies",
alliance_ticker="WYT",
)
# when
update_character_chunk([character_1001.character_id])
# then
self.assertSetEqual(self._updated_character_ids(spy_update_character), set())
mock_provider.client.Character.post_characters_affiliation.side_effect \ def test_should_fall_back_to_single_updates_when_bulk_update_failed(
= get_affiliations self, mock_providers, mock_esi_client_factory, spy_update_character
):
mock_provider.client.Universe.post_universe_names.side_effect = get_names # given
mock_providers.provider.client.Character.post_characters_affiliation\
run_model_update() .side_effect = OSError
characters_updated = { mock_esi_client_factory.return_value = EsiClientStub()
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list character_1001 = EveCharacter.objects.create(
} character_id=1001,
excepted = {1, 3} character_name="Bruce Wayne",
self.assertSetEqual(characters_updated, excepted) corporation_id=2001,
corporation_name="Wayne Technologies",
corporation_ticker="WTE",
alliance_id=3001,
alliance_name="Wayne Technologies",
alliance_ticker="WYT",
)
# when
update_character_chunk([character_1001.character_id])
# then
self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})

View File

@@ -1,14 +1,18 @@
from django import forms
from django.apps import apps from django.apps import apps
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup, User from django.contrib.auth.models import Group as BaseGroup, User
from django.core.exceptions import ValidationError
from django.db.models import Count from django.db.models import Count
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.db.models.signals import pre_save, post_save, pre_delete, \ from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed post_delete, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from .models import AuthGroup from .models import AuthGroup, ReservedGroupName
from .models import GroupRequest from .models import GroupRequest
if 'eve_autogroups' in apps.app_configs: if 'eve_autogroups' in apps.app_configs:
@@ -70,8 +74,7 @@ if _has_auto_groups:
managedalliancegroup__isnull=True, managedalliancegroup__isnull=True,
managedcorpgroup__isnull=True managedcorpgroup__isnull=True
) )
else: return queryset
return queryset
class HasLeaderFilter(admin.SimpleListFilter): class HasLeaderFilter(admin.SimpleListFilter):
@@ -90,11 +93,22 @@ class HasLeaderFilter(admin.SimpleListFilter):
return queryset.filter(authgroup__group_leaders__isnull=False) return queryset.filter(authgroup__group_leaders__isnull=False)
elif value == 'no': elif value == 'no':
return queryset.filter(authgroup__group_leaders__isnull=True) return queryset.filter(authgroup__group_leaders__isnull=True)
else: return queryset
return queryset
class GroupAdminForm(forms.ModelForm):
def clean_name(self):
my_name = self.cleaned_data['name']
if ReservedGroupName.objects.filter(name__iexact=my_name).exists():
raise ValidationError(
_("This name has been reserved and can not be used for groups."),
code='reserved_name'
)
return my_name
class GroupAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin):
form = GroupAdminForm
list_select_related = ('authgroup',) list_select_related = ('authgroup',)
ordering = ('name',) ordering = ('name',)
list_display = ( list_display = (
@@ -209,6 +223,41 @@ class GroupRequestAdmin(admin.ModelAdmin):
_leave_request.boolean = True _leave_request.boolean = True
class ReservedGroupNameAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created_by'].initial = self.current_user.username
self.fields['created_at'].initial = _("(auto)")
created_by = forms.CharField(disabled=True)
created_at = forms.CharField(disabled=True)
def clean_name(self):
my_name = self.cleaned_data['name'].lower()
if Group.objects.filter(name__iexact=my_name).exists():
raise ValidationError(
_("There already exists a group with that name."), code='already_exists'
)
return my_name
def clean_created_at(self):
return now()
@admin.register(ReservedGroupName)
class ReservedGroupNameAdmin(admin.ModelAdmin):
form = ReservedGroupNameAdminForm
list_display = ("name", "created_by", "created_at")
def get_form(self, request, *args, **kwargs):
form = super().get_form(request, *args, **kwargs)
form.current_user = request.user
return form
def has_change_permission(self, *args, **kwargs) -> bool:
return False
@receiver(pre_save, sender=Group) @receiver(pre_save, sender=Group)
def redirect_pre_save(sender, signal=None, *args, **kwargs): def redirect_pre_save(sender, signal=None, *args, **kwargs):
pre_save.send(BaseGroup, *args, **kwargs) pre_save.send(BaseGroup, *args, **kwargs)

View File

@@ -14,8 +14,8 @@ class GroupManager:
@classmethod @classmethod
def get_joinable_groups_for_user( def get_joinable_groups_for_user(
cls, user: User, include_hidden = True cls, user: User, include_hidden=True
) -> QuerySet: ) -> QuerySet[Group]:
"""get groups a user could join incl. groups already joined""" """get groups a user could join incl. groups already joined"""
groups_qs = cls.get_joinable_groups(user.profile.state) groups_qs = cls.get_joinable_groups(user.profile.state)
@@ -28,24 +28,27 @@ class GroupManager:
return groups_qs return groups_qs
@staticmethod @staticmethod
def get_joinable_groups(state: State) -> QuerySet: def get_joinable_groups(state: State) -> QuerySet[Group]:
"""get groups that can be joined by user with given state""" """get groups that can be joined by user with given state"""
return Group.objects\ return (
.select_related('authgroup')\ Group.objects
.exclude(authgroup__internal=True)\
.filter(Q(authgroup__states=state) | Q(authgroup__states=None))
@staticmethod
def get_all_non_internal_groups() -> QuerySet:
"""get groups that are not internal"""
return Group.objects\
.select_related('authgroup')\
.exclude(authgroup__internal=True) .exclude(authgroup__internal=True)
.filter(Q(authgroup__states=state) | Q(authgroup__states=None))
)
@staticmethod @staticmethod
def get_group_leaders_groups(user: User): def get_all_non_internal_groups() -> QuerySet[Group]:
return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) | \ """get groups that are not internal"""
Group.objects.select_related('authgroup').filter(authgroup__group_leader_groups__in=user.groups.all()) return Group.objects.exclude(authgroup__internal=True)
@staticmethod
def get_group_leaders_groups(user: User) -> QuerySet[Group]:
return (
Group.objects.filter(authgroup__group_leaders=user)
| Group.objects.filter(
authgroup__group_leader_groups__in=list(user.groups.all())
)
)
@staticmethod @staticmethod
def joinable_group(group: Group, state: State) -> bool: def joinable_group(group: Group, state: State) -> bool:
@@ -57,12 +60,12 @@ class GroupManager:
:param state: allianceauth.authentication.State object :param state: allianceauth.authentication.State object
:return: bool True if its joinable, False otherwise :return: bool True if its joinable, False otherwise
""" """
if (len(group.authgroup.states.all()) != 0 if (
len(group.authgroup.states.all()) != 0
and state not in group.authgroup.states.all() and state not in group.authgroup.states.all()
): ):
return False return False
else: return not group.authgroup.internal
return not group.authgroup.internal
@staticmethod @staticmethod
def check_internal_group(group: Group) -> bool: def check_internal_group(group: Group) -> bool:
@@ -78,7 +81,7 @@ class GroupManager:
return user.has_perm('auth.group_management') return user.has_perm('auth.group_management')
@classmethod @classmethod
def can_manage_groups(cls, user:User ) -> bool: def can_manage_groups(cls, user:User) -> bool:
""" """
For use with user_passes_test decorator. For use with user_passes_test decorator.
Check if the user can manage groups. Either has the Check if the user can manage groups. Either has the
@@ -88,7 +91,10 @@ class GroupManager:
:return: bool True if user can manage groups, False otherwise :return: bool True if user can manage groups, False otherwise
""" """
if user.is_authenticated: if user.is_authenticated:
return cls.has_management_permission(user) or cls.get_group_leaders_groups(user) return (
cls.has_management_permission(user)
or cls.get_group_leaders_groups(user)
)
return False return False
@classmethod @classmethod
@@ -100,19 +106,19 @@ class GroupManager:
:return: True if the user can manage the group :return: True if the user can manage the group
""" """
if user.is_authenticated: if user.is_authenticated:
return cls.has_management_permission(user) or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists() return (
cls.has_management_permission(user)
or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
)
return False return False
@classmethod @classmethod
def pending_requests_count_for_user(cls, user: User) -> int: def pending_requests_count_for_user(cls, user: User) -> int:
"""Returns the number of pending group requests for the given user""" """Returns the number of pending group requests for the given user"""
if cls.has_management_permission(user): if cls.has_management_permission(user):
return GroupRequest.objects.all().count() return GroupRequest.objects.all().count()
else: return (
return ( GroupRequest.objects
GroupRequest.objects .filter(group__in=list(cls.get_group_leaders_groups(user)))
.filter(group__authgroup__group_leaders__exact=user) .count()
.select_related("group__authgroup__group_leaders") )
.count()
)

View File

@@ -0,0 +1,42 @@
# Generated by Django 3.2.9 on 2021-11-11 15:56
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('auth', '0012_alter_user_first_name_max_length'),
('authentication', '0019_merge_20211026_0919'),
('groupmanagement', '0016_remove_grouprequest_status_field'),
]
operations = [
migrations.AlterField(
model_name='authgroup',
name='group_leader_groups',
field=models.ManyToManyField(blank=True, help_text='Members of leader groups can process requests for this group. Use the <code>auth.group_management</code> permission to allow a user to manage all groups.<br>', related_name='leads_group_groups', to='auth.Group'),
),
migrations.AlterField(
model_name='authgroup',
name='group_leaders',
field=models.ManyToManyField(blank=True, help_text='Group leaders can process requests for this group. Use the <code>auth.group_management</code> permission to allow a user to manage all groups.<br>', related_name='leads_groups', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='authgroup',
name='open',
field=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.'),
),
migrations.AlterField(
model_name='authgroup',
name='public',
field=models.BooleanField(default=False, help_text='Group is public. Any registered user is able to join this group, with visibility based on the other options set for this group.<br>Auth will not remove users from this group automatically when they are no longer authenticated.'),
),
migrations.AlterField(
model_name='authgroup',
name='states',
field=models.ManyToManyField(blank=True, help_text='States listed here will have the ability to join this group provided they have the proper permissions.<br>', related_name='valid_states', to='authentication.State'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.9 on 2021-11-25 18:38
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('groupmanagement', '0017_improve_groups_documentation'),
]
operations = [
migrations.CreateModel(
name='ReservedGroupName',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name that can not be used for groups.', max_length=150, unique=True, verbose_name='name')),
('reason', models.TextField(help_text='Reason why this name is reserved.', verbose_name='reason')),
('created_by', models.CharField(help_text='Name of the user who created this entry.', max_length=255, verbose_name='created by')),
('created_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this entry was created', verbose_name='created at')),
],
),
]

View File

@@ -1,16 +1,25 @@
from typing import Set
from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_save from django.utils.timezone import now
from django.dispatch import receiver from django.utils.translation import gettext_lazy as _
from allianceauth.authentication.models import State from allianceauth.authentication.models import State
from allianceauth.notifications import notify
class GroupRequest(models.Model): class GroupRequest(models.Model):
"""Request from a user for joining or leaving a group."""
leave_request = models.BooleanField(default=0) leave_request = models.BooleanField(default=0)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
def __str__(self):
return self.user.username + ":" + self.group.name
@property @property
def main_char(self): def main_char(self):
""" """
@@ -19,11 +28,22 @@ class GroupRequest(models.Model):
""" """
return self.user.profile.main_character return self.user.profile.main_character
def __str__(self): def notify_leaders(self) -> None:
return self.user.username + ":" + self.group.name """Send notification to all group leaders about this request.
Note: No translations, because language for each leader is unknown
"""
if not getattr(settings, 'GROUPMANAGEMENT_REQUESTS_NOTIFICATION', False):
return
keyword = "leave" if self.leave_request else "join"
title = f"Group Management: {keyword.title()} request for {self.group.name}"
message = f"{self.user} want's to {keyword} {self.group.name}."
for appover in self.group.authgroup.group_request_approvers():
notify(user=appover, title=title, message=message, level="info")
class RequestLog(models.Model): class RequestLog(models.Model):
"""Log entry about who joined and left a group and who approved it."""
request_type = models.BooleanField(null=True) request_type = models.BooleanField(null=True)
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
request_info = models.CharField(max_length=254) request_info = models.CharField(max_length=254)
@@ -61,11 +81,12 @@ class AuthGroup(models.Model):
e.g. group.authgroup.internal e.g. group.authgroup.internal
Logic: Logic:
Internal - not requestable by users, at all. Covers Corp_, Alliance_, Members etc groups. Internal - not requestable by users, at all. Covers Corp_, Alliance_,
Groups are internal by default Members etc groups. Groups are internal by default
Public - Other options are respected, but any user will be able to become and remain a member, even if they Public - Other options are respected, but any user will be able to become
have no API etc entered. Auth will not manage these groups automatically so user removal is up to and remain a member, even if they have no API etc entered.
Auth will not manage these groups automatically so user removal is up to
group managers/leaders. group managers/leaders.
Not Internal and: Not Internal and:
@@ -75,60 +96,119 @@ class AuthGroup(models.Model):
Not Open - Users requests must be approved before they are added to the group Not Open - Users requests must be approved before they are added to the group
""" """
group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True) group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True)
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."
)
)
public = models.BooleanField(
default=False,
help_text=_(
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>"
"Auth will not remove users from this group automatically when they "
"are no longer authenticated."
)
)
group_leaders = models.ManyToManyField(
User,
related_name='leads_groups',
blank=True,
help_text=_(
"Group leaders can process requests for this group. "
"Use the <code>auth.group_management</code> permission to allow "
"a user to manage all groups.<br>"
)
)
group_leader_groups = models.ManyToManyField(
Group,
related_name='leads_group_groups',
blank=True,
help_text=_(
"Members of leader groups can process requests for this group. "
"Use the <code>auth.group_management</code> permission "
"to allow a user to manage all groups.<br>")
)
states = models.ManyToManyField(
State,
related_name='valid_states',
blank=True,
help_text=_(
"States listed here will have the ability to join this group provided "
"they have the proper permissions.<br>"
)
)
description = models.TextField(
max_length=512,
blank=True,
help_text=_(
"Short description <i>(max. 512 characters)</i> "
"of the group shown to users."
)
)
internal = models.BooleanField(default=True, class Meta:
help_text="Internal group, users cannot see, join or request to join this group.<br>" permissions = (
"Used for groups such as Members, Corp_*, Alliance_* etc.<br>" ("request_groups", _("Can request non-public groups")),
"<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.") default_permissions = tuple()
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.")
public = models.BooleanField(default=False,
help_text="Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br> Auth will "
"not remove users from this group automatically when they are no longer "
"authenticated.")
# Group leaders have management access to this group
group_leaders = models.ManyToManyField(User, related_name='leads_groups', 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.")
# allow groups to be *group leads*
group_leader_groups = models.ManyToManyField(Group, related_name='leads_group_groups', 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.")
states = models.ManyToManyField(State, related_name='valid_states', blank=True,
help_text="States listed here will have the ability to join this group provided "
"they have the proper permissions.")
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)"
"</i> of the group shown to users.")
def __str__(self): def __str__(self):
return self.group.name return self.group.name
class Meta: def group_request_approvers(self) -> Set[User]:
permissions = ( """Return all users who can approve a group request."""
("request_groups", "Can request non-public groups"), return set(
self.group_leaders.all()
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
) )
default_permissions = tuple()
@receiver(post_save, sender=Group) class ReservedGroupName(models.Model):
def create_auth_group(sender, instance, created, **kwargs): """Name that can not be used for groups.
"""
Creates the AuthGroup model when a group is created
"""
if created:
AuthGroup.objects.create(group=instance)
This enables AA to ignore groups on other services (e.g. Discord) with that name.
"""
name = models.CharField(
_('name'),
max_length=150,
unique=True,
help_text=_("Name that can not be used for groups.")
)
reason = models.TextField(
_('reason'), help_text=_("Reason why this name is reserved.")
)
created_by = models.CharField(
_('created by'),
max_length=255,
help_text="Name of the user who created this entry."
)
created_at = models.DateTimeField(
_('created at'), default=now, help_text=_("Date when this entry was created")
)
@receiver(post_save, sender=Group) def __str__(self) -> str:
def save_auth_group(sender, instance, **kwargs): return self.name
"""
Ensures AuthGroup model is saved automatically def save(self, *args, **kwargs) -> None:
""" if Group.objects.filter(name__iexact=self.name).exists():
instance.authgroup.save() raise RuntimeError(
f"Save failed. There already exists a group with the name: {self.name}."
)
super().save(*args, **kwargs)

View File

@@ -1,11 +1,33 @@
import logging import logging
from django.contrib.auth.models import Group
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver from django.dispatch import receiver
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import state_changed
from .models import AuthGroup, ReservedGroupName
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@receiver(pre_save, sender=Group)
def find_new_name_for_conflicting_groups(sender, instance, **kwargs):
"""Find new name for a group which name is already reserved."""
new_name = instance.name
num = 0
while ReservedGroupName.objects.filter(name__iexact=new_name).exists():
num += 1
new_name = f"{instance.name}_{num}"
instance.name = new_name
@receiver(post_save, sender=Group)
def create_auth_group(sender, instance, created, **kwargs):
"""Create the AuthGroup model when a group is created."""
if created:
AuthGroup.objects.create(group=instance)
@receiver(state_changed) @receiver(state_changed)
def check_groups_on_state_change(sender, user, state, **kwargs): def check_groups_on_state_change(sender, user, state, **kwargs):
logger.debug( logger.debug(

View File

@@ -127,6 +127,8 @@
], ],
bootstrap: true bootstrap: true
}, },
"stateSave": true,
"stateDuration": 0
}); });
}); });
{% endblock %} {% endblock %}

View File

@@ -104,7 +104,9 @@
"sortable": false, "sortable": false,
"targets": [2] "targets": [2]
}, },
] ],
"stateSave": true,
"stateDuration": 0
}); });
}); });
{% endblock %} {% endblock %}

View File

@@ -29,15 +29,18 @@
{% endif %} {% endif %}
</a> </a>
</li> </li>
<li>
<a data-toggle="tab" href="#leave">
{% translate "Leave Requests" %}
{% if leaverequests %} {% if not auto_leave %}
<span class="badge">{{ leaverequests|length }}</span> <li>
{% endif %} <a data-toggle="tab" href="#leave">
</a> {% translate "Leave Requests" %}
</li>
{% if leaverequests %}
<span class="badge">{{ leaverequests|length }}</span>
{% endif %}
</a>
</li>
{% endif %}
</ul> </ul>
<div class="panel panel-default panel-tabs-aa"> <div class="panel panel-default panel-tabs-aa">
@@ -100,61 +103,63 @@
{% endif %} {% endif %}
</div> </div>
<div id="leave" class="tab-pane"> {% if not auto_leave %}
{% if leaverequests %} <div id="leave" class="tab-pane">
<div class="table-responsive"> {% if leaverequests %}
<table class="table table-aa"> <div class="table-responsive">
<thead> <table class="table table-aa">
<tr> <thead>
<th>{% translate "Character" %}</th>
<th>{% translate "Organization" %}</th>
<th>{% translate "Group" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for leaverequest in leaverequests %}
<tr> <tr>
<td> <th>{% translate "Character" %}</th>
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;"> <th>{% translate "Organization" %}</th>
{% if leaverequest.main_char %} <th>{% translate "Group" %}</th>
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank"> <th></th>
{{ 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> </tr>
{% endfor %} </thead>
</tbody>
</table> <tbody>
</div> {% for leaverequest in leaverequests %}
{% else %} <tr>
<div class="alert alert-warning text-center">{% translate "No group leave requests." %}</div> <td>
{% endif %} <img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
</div> {% 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 %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,9 +10,10 @@ from allianceauth.authentication.models import CharacterOwnership, State
from allianceauth.eveonline.models import ( from allianceauth.eveonline.models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo EveCharacter, EveCorporationInfo, EveAllianceInfo
) )
from ..admin import HasLeaderFilter, GroupAdmin, Group from ..admin import HasLeaderFilter, GroupAdmin, Group
from . import get_admin_change_view_url from . import get_admin_change_view_url
from ..models import ReservedGroupName
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS: if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True _has_auto_groups = True
@@ -396,3 +397,108 @@ class TestGroupAdmin(TestCase):
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
response = c.get(get_admin_change_view_url(self.group_1)) response = c.get(get_admin_change_view_url(self.group_1))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_should_create_new_group(self):
# given
user = User.objects.create_superuser("bruce")
self.client.force_login(user)
# when
response = self.client.post(
"/admin/groupmanagement/group/add/",
data={
"name": "new group",
"authgroup-TOTAL_FORMS": 1,
"authgroup-INITIAL_FORMS": 0,
"authgroup-MIN_NUM_FORMS": 0,
"authgroup-MAX_NUM_FORMS": 1,
}
)
# then
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/groupmanagement/group/")
self.assertTrue(Group.objects.filter(name="new group").exists())
def test_should_not_allow_creating_new_group_with_reserved_name(self):
# given
ReservedGroupName.objects.create(
name="new group", reason="dummy", created_by="bruce"
)
user = User.objects.create_superuser("bruce")
self.client.force_login(user)
# when
response = self.client.post(
"/admin/groupmanagement/group/add/",
data={
"name": "New group",
"authgroup-TOTAL_FORMS": 1,
"authgroup-INITIAL_FORMS": 0,
"authgroup-MIN_NUM_FORMS": 0,
"authgroup-MAX_NUM_FORMS": 1,
}
)
# then
self.assertContains(
response, "This name has been reserved and can not be used for groups"
)
self.assertFalse(Group.objects.filter(name="new group").exists())
def test_should_not_allow_changing_name_of_existing_group_to_reserved_name(self):
# given
ReservedGroupName.objects.create(
name="new group", reason="dummy", created_by="bruce"
)
group = Group.objects.create(name="dummy")
user = User.objects.create_superuser("bruce")
self.client.force_login(user)
# when
response = self.client.post(
f"/admin/groupmanagement/group/{group.pk}/change/",
data={
"name": "new group",
"authgroup-TOTAL_FORMS": 1,
"authgroup-INITIAL_FORMS": 0,
"authgroup-MIN_NUM_FORMS": 0,
"authgroup-MAX_NUM_FORMS": 1,
}
)
# then
self.assertContains(
response, "This name has been reserved and can not be used for groups"
)
self.assertFalse(Group.objects.filter(name="new group").exists())
class TestReservedGroupNameAdmin(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = User.objects.create_superuser("bruce")
def test_should_create_new_entry(self):
# given
self.client.force_login(self.user)
# when
response = self.client.post(
"/admin/groupmanagement/reservedgroupname/add/",
data={"name": "Test", "reason": "dummy"}
)
# then
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/groupmanagement/reservedgroupname/")
obj = ReservedGroupName.objects.get(name="test")
self.assertEqual(obj.name, "test")
self.assertEqual(obj.created_by, self.user.username)
self.assertTrue(obj.created_at)
def test_should_not_allow_names_of_existing_groups(self):
# given
Group.objects.create(name="Already taken")
self.client.force_login(self.user)
# when
response = self.client.post(
"/admin/groupmanagement/reservedgroupname/add/",
data={"name": "already taken", "reason": "dummy"}
)
# then
self.assertContains(response, "There already exists a group with that name")
self.assertFalse(ReservedGroupName.objects.filter(name="already taken").exists())

View File

@@ -1,8 +1,5 @@
from unittest.mock import Mock, patch
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
@@ -44,9 +41,9 @@ class GroupManagementVisibilityTestCase(TestCase):
self._refresh_user() self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user) groups = GroupManager.get_group_leaders_groups(self.user)
self.assertIn(self.group1, groups) #avail due to user self.assertIn(self.group1, groups) # avail due to user
self.assertNotIn(self.group2, groups) #not avail due to group self.assertNotIn(self.group2, groups) # not avail due to group
self.assertNotIn(self.group3, groups) #not avail at all self.assertNotIn(self.group3, groups) # not avail at all
self.user.groups.add(self.group1) self.user.groups.add(self.group1)
self._refresh_user() self._refresh_user()
@@ -71,70 +68,66 @@ class GroupManagementVisibilityTestCase(TestCase):
class TestGroupManager(TestCase): class TestGroupManager(TestCase):
def setUp(self) -> None:
@classmethod
def setUpClass(cls):
super().setUpClass()
# group 1 # group 1
cls.group_default = Group.objects.create(name='default') self.group_default = Group.objects.create(name='default')
cls.group_default.authgroup.description = 'Default Group' self.group_default.authgroup.description = 'Default Group'
cls.group_default.authgroup.internal = False self.group_default.authgroup.internal = False
cls.group_default.authgroup.hidden = False self.group_default.authgroup.hidden = False
cls.group_default.authgroup.save() self.group_default.authgroup.save()
# group 2 # group 2
cls.group_internal = Group.objects.create(name='internal') self.group_internal = Group.objects.create(name='internal')
cls.group_internal.authgroup.description = 'Internal Group' self.group_internal.authgroup.description = 'Internal Group'
cls.group_internal.authgroup.internal = True self.group_internal.authgroup.internal = True
cls.group_internal.authgroup.save() self.group_internal.authgroup.save()
# group 3 # group 3
cls.group_hidden = Group.objects.create(name='hidden') self.group_hidden = Group.objects.create(name='hidden')
cls.group_hidden.authgroup.description = 'Hidden Group' self.group_hidden.authgroup.description = 'Hidden Group'
cls.group_hidden.authgroup.internal = False self.group_hidden.authgroup.internal = False
cls.group_hidden.authgroup.hidden = True self.group_hidden.authgroup.hidden = True
cls.group_hidden.authgroup.save() self.group_hidden.authgroup.save()
# group 4 # group 4
cls.group_open = Group.objects.create(name='open') self.group_open = Group.objects.create(name='open')
cls.group_open.authgroup.description = 'Open Group' self.group_open.authgroup.description = 'Open Group'
cls.group_open.authgroup.internal = False self.group_open.authgroup.internal = False
cls.group_open.authgroup.hidden = False self.group_open.authgroup.hidden = False
cls.group_open.authgroup.open = True self.group_open.authgroup.open = True
cls.group_open.authgroup.save() self.group_open.authgroup.save()
# group 5 # group 5
cls.group_public_1 = Group.objects.create(name='public 1') self.group_public_1 = Group.objects.create(name='public 1')
cls.group_public_1.authgroup.description = 'Public Group 1' self.group_public_1.authgroup.description = 'Public Group 1'
cls.group_public_1.authgroup.internal = False self.group_public_1.authgroup.internal = False
cls.group_public_1.authgroup.hidden = False self.group_public_1.authgroup.hidden = False
cls.group_public_1.authgroup.public = True self.group_public_1.authgroup.public = True
cls.group_public_1.authgroup.save() self.group_public_1.authgroup.save()
# group 6 # group 6
cls.group_public_2 = Group.objects.create(name='public 2') self.group_public_2 = Group.objects.create(name='public 2')
cls.group_public_2.authgroup.description = 'Public Group 2' self.group_public_2.authgroup.description = 'Public Group 2'
cls.group_public_2.authgroup.internal = False self.group_public_2.authgroup.internal = False
cls.group_public_2.authgroup.hidden = True self.group_public_2.authgroup.hidden = True
cls.group_public_2.authgroup.open = True self.group_public_2.authgroup.open = True
cls.group_public_2.authgroup.public = True self.group_public_2.authgroup.public = True
cls.group_public_2.authgroup.save() self.group_public_2.authgroup.save()
# group 7 # group 7
cls.group_default_member = Group.objects.create(name='default members') self.group_default_member = Group.objects.create(name='default members')
cls.group_default_member.authgroup.description = \ self.group_default_member.authgroup.description = \
'Default Group for members only' 'Default Group for members only'
cls.group_default_member.authgroup.internal = False self.group_default_member.authgroup.internal = False
cls.group_default_member.authgroup.hidden = False self.group_default_member.authgroup.hidden = False
cls.group_default_member.authgroup.open = False self.group_default_member.authgroup.open = False
cls.group_default_member.authgroup.public = False self.group_default_member.authgroup.public = False
cls.group_default_member.authgroup.states.add( self.group_default_member.authgroup.states.add(
AuthUtils.get_member_state() AuthUtils.get_member_state()
) )
cls.group_default_member.authgroup.save() self.group_default_member.authgroup.save()
def setUp(self): # user
self.user = AuthUtils.create_user('Bruce Wayne') self.user = AuthUtils.create_user('Bruce Wayne')
def test_get_joinable_group_member(self): def test_get_joinable_group_member(self):
@@ -241,7 +234,7 @@ class TestGroupManager(TestCase):
def test_get_joinable_groups_for_user_member_w_permission(self): def test_get_joinable_groups_for_user_member_w_permission(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True) AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True)
AuthUtils.add_permission_to_user_by_name( self.user = AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user 'groupmanagement.request_groups', self.user
) )
result = GroupManager.get_joinable_groups_for_user(self.user) result = GroupManager.get_joinable_groups_for_user(self.user)
@@ -257,7 +250,7 @@ class TestGroupManager(TestCase):
def test_get_joinable_groups_for_user_member_w_permission_no_hidden(self): def test_get_joinable_groups_for_user_member_w_permission_no_hidden(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True) AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True)
AuthUtils.add_permission_to_user_by_name( self.user = AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user 'groupmanagement.request_groups', self.user
) )
result = GroupManager.get_joinable_groups_for_user( result = GroupManager.get_joinable_groups_for_user(
@@ -273,7 +266,7 @@ class TestGroupManager(TestCase):
def test_has_management_permission(self): def test_has_management_permission(self):
user = AuthUtils.create_user('Clark Kent') user = AuthUtils.create_user('Clark Kent')
AuthUtils.add_permission_to_user_by_name( user = AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user 'auth.group_management', user
) )
self.assertTrue(GroupManager.has_management_permission(user)) self.assertTrue(GroupManager.has_management_permission(user))
@@ -288,7 +281,7 @@ class TestGroupManager(TestCase):
def test_can_manage_groups_has_perm(self): def test_can_manage_groups_has_perm(self):
user = AuthUtils.create_user('Clark Kent') user = AuthUtils.create_user('Clark Kent')
AuthUtils.add_permission_to_user_by_name( user = AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user 'auth.group_management', user
) )
self.assertTrue(GroupManager.can_manage_groups(user)) self.assertTrue(GroupManager.can_manage_groups(user))
@@ -306,7 +299,7 @@ class TestGroupManager(TestCase):
def test_can_manage_group_has_perm(self): def test_can_manage_group_has_perm(self):
user = AuthUtils.create_user('Clark Kent') user = AuthUtils.create_user('Clark Kent')
AuthUtils.add_permission_to_user_by_name( user = AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user 'auth.group_management', user
) )
self.assertTrue( self.assertTrue(
@@ -433,11 +426,21 @@ class TestPendingRequestsCountForUser(TestCase):
# when user_requestor is requesting access to group 1 # when user_requestor is requesting access to group 1
# then return 1 for user_leader_4 # then return 1 for user_leader_4
user_leader_4 = AuthUtils.create_member("Lex Luther") user_leader_4 = AuthUtils.create_member("Lex Luther")
AuthUtils.add_permission_to_user_by_name("auth.group_management", user_leader_4) user_leader_4 = AuthUtils.add_permission_to_user_by_name(
user_leader_4 = User.objects.get(pk=user_leader_4.pk) "auth.group_management", user_leader_4
GroupRequest.objects.create(
user=self.user_requestor, group=self.group_1
) )
GroupRequest.objects.create(user=self.user_requestor, group=self.group_1)
self.assertEqual( self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1 GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
) )
def test_single_request_for_members_of_leading_group(self):
# given
leader_group = Group.objects.create(name="Leaders")
self.group_3.authgroup.group_leader_groups.add(leader_group)
self.user_leader_1.groups.add(leader_group)
GroupRequest.objects.create(user=self.user_requestor, group=self.group_3)
# when
result = GroupManager.pending_requests_count_for_user(self.user_leader_1)
# then
self.assertEqual(result, 1)

View File

@@ -1,31 +1,22 @@
from unittest import mock from unittest import mock
from django.contrib.auth.models import User, Group from django.contrib.auth.models import Group
from django.test import TestCase from django.test import TestCase, override_settings
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import (
EveCorporationInfo, EveAllianceInfo, EveCharacter
)
from ..models import GroupRequest, RequestLog from ..models import GroupRequest, RequestLog, ReservedGroupName
MODULE_PATH = "allianceauth.groupmanagement.models"
def create_testdata(): def create_testdata():
# clear DB
User.objects.all().delete()
Group.objects.all().delete()
EveCharacter.objects.all().delete()
EveCorporationInfo.objects.all().delete()
EveAllianceInfo.objects.all().delete()
# group 1 # group 1
group = Group.objects.create(name='Superheros') group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group' group.authgroup.description = 'Default Group'
group.authgroup.internal = False group.authgroup.internal = False
group.authgroup.hidden = False group.authgroup.hidden = False
group.authgroup.save() group.authgroup.save()
# user 1 # user 1
user_1 = AuthUtils.create_user('Bruce Wayne') user_1 = AuthUtils.create_user('Bruce Wayne')
AuthUtils.add_main_character_2( AuthUtils.add_main_character_2(
@@ -37,7 +28,6 @@ def create_testdata():
) )
user_1.groups.add(group) user_1.groups.add(group)
group.authgroup.group_leaders.add(user_1) group.authgroup.group_leaders.add(user_1)
# user 2 # user 2
user_2 = AuthUtils.create_user('Clark Kent') user_2 = AuthUtils.create_user('Clark Kent')
AuthUtils.add_main_character_2( AuthUtils.add_main_character_2(
@@ -45,18 +35,25 @@ def create_testdata():
name='Clark Kent', name='Clark Kent',
character_id=1002, character_id=1002,
corp_id=2002, corp_id=2002,
corp_name='Wayne Technologies' corp_name='Wayne Food'
) )
return group, user_1, user_2 # user 3
user_3 = AuthUtils.create_user('Peter Parker')
AuthUtils.add_main_character_2(
user_2,
name='Peter Parker',
character_id=1003,
corp_id=2002,
corp_name='Wayne Food'
)
return group, user_1, user_2, user_3
class TestGroupRequest(TestCase): class TestGroupRequest(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.group, cls.user_1, _ = create_testdata() cls.group, cls.user_1, cls.user_2, cls.user_3 = create_testdata()
def test_main_char(self): def test_main_char(self):
group_request = GroupRequest.objects.create( group_request = GroupRequest.objects.create(
@@ -74,13 +71,85 @@ class TestGroupRequest(TestCase):
expected = 'Bruce Wayne:Superheros' expected = 'Bruce Wayne:Superheros'
self.assertEqual(str(group_request), expected) self.assertEqual(str(group_request), expected)
@override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
def test_should_notify_leaders_about_join_request(self):
# given
group_request = GroupRequest.objects.create(
user=self.user_2, group=self.group
)
# when
with mock.patch(MODULE_PATH + ".notify") as mock_notify:
group_request.notify_leaders()
# then
self.assertTrue(mock_notify.called)
_, kwargs = mock_notify.call_args
self.assertEqual(kwargs["user"],self.user_1)
@override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
def test_should_notify_leaders_about_leave_request(self):
# given
group_request = GroupRequest.objects.create(
user=self.user_2, group=self.group
)
# when
with mock.patch(MODULE_PATH + ".notify") as mock_notify:
group_request.notify_leaders()
# then
self.assertTrue(mock_notify.called)
@override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
def test_should_handle_notify_leaders_without_leaders(self):
# given
group = Group.objects.create(name='Dummy')
group.authgroup.internal = False
group.authgroup.hidden = False
group.authgroup.save()
group_request = GroupRequest.objects.create(
user=self.user_2, group=group
)
# when
with mock.patch(MODULE_PATH + ".notify") as mock_notify:
group_request.notify_leaders()
# then
self.assertFalse(mock_notify.called)
@override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=False)
def test_should_not_notify_leaders_if_disabled(self):
# given
group_request = GroupRequest.objects.create(
user=self.user_2, group=self.group
)
# when
with mock.patch(MODULE_PATH + ".notify") as mock_notify:
group_request.notify_leaders()
# then
self.assertFalse(mock_notify.called)
@override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
def test_should_notify_members_of_leader_groups_about_join_request(self):
# given
child_group = Group.objects.create(name='Child')
child_group.authgroup.internal = False
child_group.authgroup.hidden = False
child_group.authgroup.save()
child_group.authgroup.group_leader_groups.add(self.group)
group_request = GroupRequest.objects.create(
user=self.user_2, group=child_group
)
# when
with mock.patch(MODULE_PATH + ".notify") as mock_notify:
group_request.notify_leaders()
# then
self.assertTrue(mock_notify.called)
_, kwargs = mock_notify.call_args
self.assertEqual(kwargs["user"],self.user_1)
class TestRequestLog(TestCase): class TestRequestLog(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.group, cls.user_1, cls.user_2 = create_testdata() cls.group, cls.user_1, cls.user_2, _ = create_testdata()
def test_requestor(self): def test_requestor(self):
request_log = RequestLog.objects.create( request_log = RequestLog.objects.create(
@@ -126,7 +195,7 @@ class TestRequestLog(TestCase):
group=self.group, group=self.group,
request_info='Clark Kent:Superheros', request_info='Clark Kent:Superheros',
request_actor=self.user_1, request_actor=self.user_1,
action = True action=True
) )
expected = 'Accept' expected = 'Accept'
self.assertEqual(request_log.action_to_str(), expected) self.assertEqual(request_log.action_to_str(), expected)
@@ -136,7 +205,7 @@ class TestRequestLog(TestCase):
group=self.group, group=self.group,
request_info='Clark Kent:Superheros', request_info='Clark Kent:Superheros',
request_actor=self.user_1, request_actor=self.user_1,
action = False action=False
) )
expected = 'Reject' expected = 'Reject'
self.assertEqual(request_log.action_to_str(), expected) self.assertEqual(request_log.action_to_str(), expected)
@@ -146,14 +215,13 @@ class TestRequestLog(TestCase):
group=self.group, group=self.group,
request_info='Clark Kent:Superheros', request_info='Clark Kent:Superheros',
request_actor=self.user_1, request_actor=self.user_1,
action = False action=False
) )
expected = self.user_2.profile.main_character expected = self.user_2.profile.main_character
self.assertEqual(request_log.req_char(), expected) self.assertEqual(request_log.req_char(), expected)
class TestAuthGroup(TestCase): class TestAuthGroup(TestCase):
def test_str(self): def test_str(self):
group = Group.objects.create(name='Superheros') group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group' group.authgroup.description = 'Default Group'
@@ -163,3 +231,75 @@ class TestAuthGroup(TestCase):
expected = 'Superheros' expected = 'Superheros'
self.assertEqual(str(group.authgroup), expected) self.assertEqual(str(group.authgroup), expected)
class TestAuthGroupRequestApprovers(TestCase):
def setUp(self) -> None:
self.group, self.user_1, self.user_2, self.user_3 = create_testdata()
def test_should_return_leaders_of_main_group_only(self):
# when
leaders = self.group.authgroup.group_request_approvers()
# then
self.assertSetEqual(leaders, {self.user_1})
def test_should_return_members_of_leading_groups_only(self):
# given
parent_group = Group.objects.create(name='Parent')
parent_group.authgroup.group_leaders.add(self.user_2)
self.user_1.groups.add(parent_group)
child_group = Group.objects.create(name='Child')
child_group.authgroup.internal = False
child_group.authgroup.hidden = False
child_group.authgroup.save()
child_group.authgroup.group_leader_groups.add(parent_group)
# when
leaders = child_group.authgroup.group_request_approvers()
# then
self.assertSetEqual(leaders, {self.user_1})
def test_should_return_leaders_of_main_group_and_members_of_leading_groups(self):
# given
parent_group = Group.objects.create(name='Parent')
parent_group.authgroup.group_leaders.add(self.user_2)
self.user_1.groups.add(parent_group)
child_group = Group.objects.create(name='Child')
child_group.authgroup.internal = False
child_group.authgroup.hidden = False
child_group.authgroup.save()
child_group.authgroup.group_leaders.add(self.user_3)
child_group.authgroup.group_leader_groups.add(self.group)
# when
leaders = child_group.authgroup.group_request_approvers()
# then
self.assertSetEqual(leaders, {self.user_1, self.user_3})
def test_can_handle_group_without_leaders(self):
# given
child_group = Group.objects.create(name='Child')
child_group.authgroup.internal = False
child_group.authgroup.hidden = False
child_group.authgroup.save()
# when
leaders = child_group.authgroup.group_request_approvers()
# then
self.assertSetEqual(leaders, set())
class TestReservedGroupName(TestCase):
def test_should_return_name(self):
# given
obj = ReservedGroupName(name="test", reason="abc", created_by="xxx")
# when
result = str(obj)
# then
self.assertEqual(result, "test")
def test_should_not_allow_creating_reserved_name_for_existing_group(self):
# given
Group.objects.create(name="Dummy")
# when
with self.assertRaises(RuntimeError):
ReservedGroupName.objects.create(
name="dummy", reason="abc", created_by="xxx"
)

View File

@@ -6,6 +6,27 @@ from allianceauth.eveonline.autogroups.models import AutogroupsConfig
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from ..models import ReservedGroupName
class TestGroupSignals(TestCase):
def test_should_create_authgroup_when_group_is_created(self):
# when
group = Group.objects.create(name="test")
# then
self.assertEqual(group.authgroup.group, group)
def test_should_rename_group_that_conflicts_with_reserved_name(self):
# given
ReservedGroupName.objects.create(name="test", reason="dummy", created_by="xyz")
ReservedGroupName.objects.create(name="test_1", reason="dummy", created_by="xyz")
# when
group = Group.objects.create(name="Test")
# then
self.assertNotEqual(group.name, "test")
self.assertNotEqual(group.name, "test_1")
class TestCheckGroupsOnStateChange(TestCase): class TestCheckGroupsOnStateChange(TestCase):
@classmethod @classmethod

View File

@@ -1,22 +1,85 @@
from unittest.mock import Mock, patch from django.test import RequestFactory, TestCase, override_settings
from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from esi.models import Token
from .. import views from .. import views
def response_content_to_str(response) -> str:
return response.content.decode(response.charset)
class TestViews(TestCase): class TestViews(TestCase):
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
self.user = AuthUtils.create_user('Bruce Wayne') self.user = AuthUtils.create_user('Peter Parker')
self.user_with_manage_permission = AuthUtils.create_user('Bruce Wayne')
# set permissions
AuthUtils.add_permission_to_user_by_name(
'auth.group_management', self.user_with_manage_permission
)
def test_groups_view_can_load(self): def test_groups_view_can_load(self):
request = self.factory.get(reverse('groupmanagement:groups')) request = self.factory.get(reverse('groupmanagement:groups'))
request.user = self.user request.user = self.user
response = views.groups_view(request) response = views.groups_view(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_management_view_can_load_for_user_with_permissions(self):
"""
Test if user with management permissions can access the management view
:return:
"""
request = self.factory.get(reverse('groupmanagement:management'))
request.user = self.user_with_manage_permission
response = views.group_management(request)
self.assertEqual(response.status_code, 200)
def test_management_view_doesnt_load_for_user_without_permissions(self):
"""
Test if user without management permissions can't access the management view
:return:
"""
request = self.factory.get(reverse('groupmanagement:management'))
request.user = self.user
response = views.group_management(request)
self.assertEqual(response.status_code, 302)
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=False)
def test_leave_requests_tab_visible(self):
"""
Test if the leave requests tab is visible when GROUPMANAGEMENT_AUTO_LEAVE = False
:return:
"""
request = self.factory.get(reverse('groupmanagement:management'))
request.user = self.user_with_manage_permission
response = views.group_management(request)
content = response_content_to_str(response)
self.assertEqual(response.status_code, 200)
self.assertIn('<a data-toggle="tab" href="#leave">', content)
self.assertIn('<div id="leave" class="tab-pane">', content)
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
def test_leave_requests_tab_hidden(self):
"""
Test if the leave requests tab is hidden when GROUPMANAGEMENT_AUTO_LEAVE = True
:return:
"""
request = self.factory.get(reverse('groupmanagement:management'))
request.user = self.user_with_manage_permission
response = views.group_management(request)
content = response_content_to_str(response)
self.assertEqual(response.status_code, 200)
self.assertNotIn('<a data-toggle="tab" href="#leave">', content)
self.assertNotIn('<div id="leave" class="tab-pane">', content)

View File

@@ -45,7 +45,11 @@ def group_management(request):
logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format( logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format(
request.user, len(acceptrequests), len(leaverequests))) request.user, len(acceptrequests), len(leaverequests)))
render_items = {'acceptrequests': acceptrequests, 'leaverequests': leaverequests} render_items = {
'acceptrequests': acceptrequests,
'leaverequests': leaverequests,
'auto_leave': getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False),
}
return render(request, 'groupmanagement/index.html', context=render_items) return render(request, 'groupmanagement/index.html', context=render_items)
@@ -359,6 +363,7 @@ def group_request_add(request, group_id):
grouprequest.leave_request = False grouprequest.leave_request = False
grouprequest.save() grouprequest.save()
logger.info(f"Created group request for user {request.user} to group {Group.objects.get(id=group_id)}") logger.info(f"Created group request for user {request.user} to group {Group.objects.get(id=group_id)}")
grouprequest.notify_leaders()
messages.success(request, _('Applied to group %(group)s.') % {"group": group}) messages.success(request, _('Applied to group %(group)s.') % {"group": group})
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")
@@ -387,7 +392,7 @@ def group_request_leave(request, group_id):
logger.info(f"{request.user} attempted to leave {group} but already has an pending leave request.") logger.info(f"{request.user} attempted to leave {group} but already has an pending leave request.")
messages.warning(request, _("You already have a pending leave request for that group.")) messages.warning(request, _("You already have a pending leave request for that group."))
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")
if getattr(settings, 'AUTO_LEAVE', False): if getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False):
logger.info(f"{request.user} leaving joinable group {group} due to auto_leave") logger.info(f"{request.user} leaving joinable group {group} due to auto_leave")
request_info = request.user.username + ":" + group.name request_info = request.user.username + ":" + group.name
log = RequestLog(request_type=True, group=group, request_info=request_info, action=1, request_actor=request.user) log = RequestLog(request_type=True, group=group, request_info=request_info, action=1, request_actor=request.user)
@@ -400,5 +405,6 @@ def group_request_leave(request, group_id):
grouprequest.leave_request = True grouprequest.leave_request = True
grouprequest.save() grouprequest.save()
logger.info(f"Created group leave request for user {request.user} to group {Group.objects.get(id=group_id)}") logger.info(f"Created group leave request for user {request.user} to group {Group.objects.get(id=group_id)}")
grouprequest.notify_leaders()
messages.success(request, _('Applied to leave group %(group)s.') % {"group": group}) messages.success(request, _('Applied to leave group %(group)s.') % {"group": group})
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")

View File

@@ -2,18 +2,18 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package. # This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
# Translators: # Translators:
# Erik Kalkoken <erik.kalkoken@gmail.com>, 2020 # Erik Kalkoken <erik.kalkoken@gmail.com>, 2020
# Joel Falknau <ozirascal@gmail.com>, 2021 # Joel Falknau <ozirascal@gmail.com>, 2021
# Peter Pfeufer <rounon.dax@terra-nanotech.de>, 2021 # Peter Pfeufer <rounon.dax@terra-nanotech.de>, 2021
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Peter Pfeufer <rounon.dax@terra-nanotech.de>, 2021\n" "Last-Translator: Peter Pfeufer <rounon.dax@terra-nanotech.de>, 2021\n"
"Language-Team: German (https://www.transifex.com/alliance-auth/teams/107430/de/)\n" "Language-Team: German (https://www.transifex.com/alliance-auth/teams/107430/de/)\n"
@@ -40,12 +40,12 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "E-Mail" msgstr "E-Mail"
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "Status geändert zu %s" msgstr "Status geändert zu %s"
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "Dein Nutzerstatus ist nun %(state)s" msgstr "Dein Nutzerstatus ist nun %(state)s"
@@ -66,29 +66,29 @@ msgstr ""
"\n" "\n"
"Hauptcharakter (Status: %(state)s)" "Hauptcharakter (Status: %(state)s)"
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "Kein Hauptcharakter gesetzt." msgstr "Kein Hauptcharakter gesetzt."
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "Charakter hinzufügen" msgstr "Charakter hinzufügen"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "Hauptcharakter ändern" msgstr "Hauptcharakter ändern"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "Gruppen" msgstr "Gruppen"
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "Charaktere" msgstr "Charaktere"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -97,13 +97,13 @@ msgstr "Charaktere"
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "Corp" msgstr "Corp"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -397,7 +397,7 @@ msgstr "Benutzername"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -476,7 +476,6 @@ msgstr "Flotte"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -485,8 +484,8 @@ msgstr "Ersteller"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "Dauer" msgstr "Dauer"
@@ -571,11 +570,128 @@ msgstr "Flottenteilnahme registriert."
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "FAT-Link ist abgelaufen." msgstr "FAT-Link ist abgelaufen."
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
"Dieser Name ist reserviert und kann nicht als Gruppenname genutzt werden."
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr "(automatisch)"
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr "Es existiert bereits eine Gruppe mit diesem Namen."
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "Gruppenverwaltung" msgstr "Gruppenverwaltung"
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
"Interne Gruppe. Nutzer können diese nicht sehen und dieser nicht beitreten. "
"<br>Dies ist für Gruppen genutzt wie Mitglieder, Corp_*, Allianz_*, "
"etc.<br><b>Überschreibt die Versteckt und Offen Option wenn gesetzt</b>"
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
"Diese Gruppe ist nicht sichtbar, aber Nutzer können dennoch beitreten wenn "
"diese den Link hierzu haben."
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
"Gruppe ist offen und Nutzer werden dieser automatisch hinzugefügt bei "
"Anfrage.<br>Wenn die Gruppe nicht offen ist, müssen Anfragen manuell "
"bestätigt werden."
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
"Öffentliche Gruppe. Jeder registrierte Nutzer kann dieser Gruppe beitreten, "
"he nach gesetzter Sichtbarkeit in den andern Optionen dieser Gruppe.<br>Auth"
" wird Nutzer nicht von dieser Gruppe entfernen wenn diese nicht länger "
"authentifiziert sind."
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
"Gruppenleiter können Anfragen für diese Gruppe bearbeiten. Nutze die "
"<code>auth.group_management</code> Berechtigung um Nutzern zu erlauben alle "
"Gruppen zu verwalten<br>"
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
"Mitglieder von Führungsgruppen können Anfragen für diese Gruppe bearbeiten. "
"Nutze die <code>auth.group_management</code> Berechtigung um Nutzern zu "
"erlauben alle Gruppen zu verwalten.<br>"
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
"Hier gelistete Ränge können dieser Gruppe beitreten, vorausgesetzt sie haben"
" die entsprechenden Berechtigungen.<br>"
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
"Kurze Beschreibung <i>(max. 512 Zeichen)</i> der Gruppe die dem Nutzer "
"angezeigt wird."
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr "Kann nicht öffentlichen Gruppen beitreten"
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr "Name"
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr "Name kann nicht für Gruppen genutzt werden"
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr "Grund"
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr "Grund wieso dieser Name reserviert ist."
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr "Erstellt bei"
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr "Erstellt"
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr "Datum der Erstellung dieses Eintrags"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -603,7 +719,7 @@ msgstr "Typ"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -669,6 +785,7 @@ msgstr "Gruppen"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
@@ -865,24 +982,24 @@ msgstr "Du bist bereits Mitglied dieser Gruppe."
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "Du hast Dich bereits für diese Gruppe beworben." msgstr "Du hast Dich bereits für diese Gruppe beworben."
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "Beitritt zur Gruppe %(group)s beantragt." msgstr "Beitritt zur Gruppe %(group)s beantragt."
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "Du kannst diese Gruppe nicht verlassen" msgstr "Du kannst diese Gruppe nicht verlassen"
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "Du bist kein Mitglied dieser Gruppe" msgstr "Du bist kein Mitglied dieser Gruppe"
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "Du hast bereits ein ausstehendes Austrittsgesuch für diese Gruppe." msgstr "Du hast bereits ein ausstehendes Austrittsgesuch für diese Gruppe."
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "Austrittsgesuch für Gruppe %(group)s gesendet." msgstr "Austrittsgesuch für Gruppe %(group)s gesendet."
@@ -1143,43 +1260,56 @@ msgstr "Alle gelesenen Benachrichtigungen gelöscht."
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "Flottenoperationen" msgstr "Flottenoperationen"
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "Doktrin" msgstr "Doktrin"
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "Startzeit" msgstr "Startzeit"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "Operationsname" msgstr "Operationsname"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr "Operationsart"
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "Flottenkommandeur" msgstr "Flottenkommandeur"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Zusätzliche Info"
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr "(Optinal) Beschreibe die Operation mit ein paar Worten"
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "Operation erstellen" msgstr "Operation erstellen"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "Form Up System" msgstr "Form Up System"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "Ortszeit" msgstr "Ortszeit"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "FC" msgstr "FC"
@@ -1197,9 +1327,8 @@ msgid "Current Eve Time:"
msgstr "Momentane Eve Zeit" msgstr "Momentane Eve Zeit"
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers" msgstr "Anstehende Flottenoperationen"
msgstr "Nächste Timer"
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
#: allianceauth/timerboard/templates/timerboard/view.html:363 #: allianceauth/timerboard/templates/timerboard/view.html:363
@@ -1207,9 +1336,8 @@ msgid "No upcoming timers."
msgstr "Keine kommenden Timer." msgstr "Keine kommenden Timer."
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers" msgstr "Vergangene Flottenoperationen"
msgstr "Vergangene Timer"
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:536 #: allianceauth/timerboard/templates/timerboard/view.html:536
@@ -1226,17 +1354,17 @@ msgstr "Aktualisiere Flottenoperationen"
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "Flottenoperation existiert nicht" msgstr "Flottenoperation existiert nicht"
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "Operation timer für %(opname)s erstellt." msgstr "Operation timer für %(opname)s erstellt."
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "Operation timer für %(opname)s entfernt." msgstr "Operation timer für %(opname)s entfernt."
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "Änderungen für Operation timer %(opname)s gespeichert." msgstr "Änderungen für Operation timer %(opname)s gespeichert."
@@ -1400,11 +1528,11 @@ msgstr "Passwort"
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "Passwort muss mindestens 8 Zeichen lang sein" msgstr "Passwort muss mindestens 8 Zeichen lang sein"
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "Discord Konto deaktiviert" msgstr "Discord Konto deaktiviert"
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1740,10 +1868,6 @@ msgstr "Flottenzeit"
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "Flottendoktrin" msgstr "Flottendoktrin"
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Zusätzliche Info"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "Killboard Link (zkillboard.com oder kb.evetools.org)" msgstr "Killboard Link (zkillboard.com oder kb.evetools.org)"
@@ -1958,12 +2082,12 @@ msgstr ""
"Der Killmail Link Deiner SRP Anfrage ist ungültig. Bitte stelle sicher, dass" "Der Killmail Link Deiner SRP Anfrage ist ungültig. Bitte stelle sicher, dass"
" Du zKillboard benutzt." " Du zKillboard benutzt."
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "SRP Anfrage für Deine %(ship)s gesendet." msgstr "SRP Anfrage für Deine %(ship)s gesendet."
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
@@ -1972,40 +2096,40 @@ msgstr ""
"Charakter %(charid)s gehört nicht zu Deinem Auth Konto. Bitte füge den API " "Charakter %(charid)s gehört nicht zu Deinem Auth Konto. Bitte füge den API "
"Key für diesen Charakter hinzu und versuche es erneut." "Key für diesen Charakter hinzu und versuche es erneut."
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "Keine SRP Anfragen ausgewählt." msgstr "Keine SRP Anfragen ausgewählt."
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "Ausgewählte SRP Anfrage konnte nicht gefunden werden." msgstr "Ausgewählte SRP Anfrage konnte nicht gefunden werden."
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "%(numrequests)s SRP Anfragen gelöscht" msgstr "%(numrequests)s SRP Anfragen gelöscht"
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "%(numrequests)s SRP Anfragen bestätigt." msgstr "%(numrequests)s SRP Anfragen bestätigt."
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "Ausgewählte SRP Anfrage konnte nicht gefunden werden." msgstr "Ausgewählte SRP Anfrage konnte nicht gefunden werden."
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "%(numrequests)s SRP Anfragen abgelehnt." msgstr "%(numrequests)s SRP Anfragen abgelehnt."
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "Unfähig SRP Anfrage mit der ID %(requestid)s zu finden." msgstr "Unfähig SRP Anfrage mit der ID %(requestid)s zu finden."
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "Änderungen der SRP Flotte %(fleetname)s gespeichert" msgstr "Änderungen der SRP Flotte %(fleetname)s gespeichert"
@@ -2232,6 +2356,14 @@ msgstr "Corp Timer"
msgid "Structure" msgid "Structure"
msgstr "Struktur" msgstr "Struktur"
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr "Nächste Timer"
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr "Vergangene Timer"
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-30 14:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -380,7 +380,7 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -459,7 +459,6 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -468,8 +467,8 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -554,11 +553,104 @@ msgstr ""
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "" msgstr ""
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "" msgstr ""
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the <code>auth."
"group_management</code> permission to allow a user to manage all groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -586,7 +678,7 @@ msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -652,6 +744,7 @@ msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "" msgstr ""
@@ -844,24 +937,24 @@ msgstr ""
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "" msgstr ""
@@ -1122,43 +1215,56 @@ msgstr ""
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr ""
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "" msgstr ""
@@ -1176,8 +1282,7 @@ msgid "Current Eve Time:"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
@@ -1186,8 +1291,7 @@ msgid "No upcoming timers."
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
@@ -1205,17 +1309,17 @@ msgstr ""
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "" msgstr ""
@@ -1379,11 +1483,11 @@ msgstr ""
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1706,10 +1810,6 @@ msgstr ""
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "" msgstr ""
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr ""
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1919,52 +2019,52 @@ msgid ""
"zKillboard." "zKillboard."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
"API key for this character and try again" "API key for this character and try again"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "" msgstr ""
@@ -2191,6 +2291,14 @@ msgstr ""
msgid "Structure" msgid "Structure"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr ""
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2021\n" "Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2021\n"
"Language-Team: Spanish (https://www.transifex.com/alliance-auth/teams/107430/es/)\n" "Language-Team: Spanish (https://www.transifex.com/alliance-auth/teams/107430/es/)\n"
@@ -39,12 +39,12 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "E-mail" msgstr "E-mail"
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "" msgstr ""
@@ -63,29 +63,29 @@ msgid ""
" " " "
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "No se ha seleccionado un personaje principal." msgstr "No se ha seleccionado un personaje principal."
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "Agregar Personaje" msgstr "Agregar Personaje"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "Cambiar Personaje Principal" msgstr "Cambiar Personaje Principal"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "Personajes" msgstr "Personajes"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -94,13 +94,13 @@ msgstr "Personajes"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "Corporación" msgstr "Corporación"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -391,7 +391,7 @@ msgstr "Usuario"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -470,7 +470,6 @@ msgstr "Flota"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -479,8 +478,8 @@ msgstr "Creador"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "Duracion" msgstr "Duracion"
@@ -565,11 +564,105 @@ msgstr "Participacion de flota registrada."
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "Enlace de participacion expirado." msgstr "Enlace de participacion expirado."
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "Manejo de Grupo" msgstr "Manejo de Grupo"
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -597,7 +690,7 @@ msgstr "Tipo"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -663,6 +756,7 @@ msgstr "Grupos"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "Descripcion" msgstr "Descripcion"
@@ -858,24 +952,24 @@ msgstr ""
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "Solicitud enviada al grupo %(group)s." msgstr "Solicitud enviada al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "No puedes dejar el grupos" msgstr "No puedes dejar el grupos"
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "No eres miembro de ese grupo" msgstr "No eres miembro de ese grupo"
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "" msgstr ""
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "Solicitaste dejar el grupo %(group)s." msgstr "Solicitaste dejar el grupo %(group)s."
@@ -1136,43 +1230,56 @@ msgstr "Se borraron todas las notificaciones leidas."
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "Operaciones de Flota" msgstr "Operaciones de Flota"
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "Doctrina" msgstr "Doctrina"
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "Tiempo de inicio" msgstr "Tiempo de inicio"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "Nombre de la operacion" msgstr "Nombre de la operacion"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "Comandante" msgstr "Comandante"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Informacion Adicional"
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "Create Operacion" msgstr "Create Operacion"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "Sistema de encuentro" msgstr "Sistema de encuentro"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "Tiempo Local" msgstr "Tiempo Local"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "Comandante" msgstr "Comandante"
@@ -1190,9 +1297,8 @@ msgid "Current Eve Time:"
msgstr "Tipo en EVE actual:" msgstr "Tipo en EVE actual:"
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers" msgstr ""
msgstr "Siguientes Timers"
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
#: allianceauth/timerboard/templates/timerboard/view.html:363 #: allianceauth/timerboard/templates/timerboard/view.html:363
@@ -1200,9 +1306,8 @@ msgid "No upcoming timers."
msgstr "No hay proximos timers." msgstr "No hay proximos timers."
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers" msgstr ""
msgstr "Timers Pasados"
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:536 #: allianceauth/timerboard/templates/timerboard/view.html:536
@@ -1219,17 +1324,17 @@ msgstr "Actualizar Operacion"
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "La operacion no existe" msgstr "La operacion no existe"
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "Se creo operacion para el timer %(opname)s." msgstr "Se creo operacion para el timer %(opname)s."
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "Se removio la operacion para %(opname)s." msgstr "Se removio la operacion para %(opname)s."
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "Se guardaron los cambios para la operacion %(opname)s" msgstr "Se guardaron los cambios para la operacion %(opname)s"
@@ -1393,11 +1498,11 @@ msgstr "Contraseña"
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "La contraseña tiene que tener 8 caracteres de largo minimo" msgstr "La contraseña tiene que tener 8 caracteres de largo minimo"
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1722,10 +1827,6 @@ msgstr "Hora de flota"
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "Doctrina" msgstr "Doctrina"
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Informacion Adicional"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1936,52 +2037,52 @@ msgid ""
msgstr "" msgstr ""
"El enalce suministrado no es valido. Por favor verifica si esats usando ZKB." "El enalce suministrado no es valido. Por favor verifica si esats usando ZKB."
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "Solicitud de SRP para tu %(ship)s completo." msgstr "Solicitud de SRP para tu %(ship)s completo."
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
"API key for this character and try again" "API key for this character and try again"
msgstr "El personaje %(charid)s no pertenece a tu cuenta" msgstr "El personaje %(charid)s no pertenece a tu cuenta"
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "No se selecciono ninguna solicitud de SRP" msgstr "No se selecciono ninguna solicitud de SRP"
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "Imposible localizar la solicitud de SRP." msgstr "Imposible localizar la solicitud de SRP."
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "Se borraron %(numrequests)s pedidos de SRP" msgstr "Se borraron %(numrequests)s pedidos de SRP"
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "Se aprobaron %(numrequests)s pedidos de SRP" msgstr "Se aprobaron %(numrequests)s pedidos de SRP"
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "Imposible localizar el pedido de SRP" msgstr "Imposible localizar el pedido de SRP"
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "Se rechazaron %(numrequests)s pedios de SRP." msgstr "Se rechazaron %(numrequests)s pedios de SRP."
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "Imposible localizar la solicitud de SRP con ID %(requestid)s" msgstr "Imposible localizar la solicitud de SRP con ID %(requestid)s"
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "Se guardaron los cambios en el SRP de la flota %(fleetname)s" msgstr "Se guardaron los cambios en el SRP de la flota %(fleetname)s"
@@ -2208,6 +2309,14 @@ msgstr "Timers de Corporacion"
msgid "Structure" msgid "Structure"
msgstr "Estructura" msgstr "Estructura"
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr "Siguientes Timers"
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr "Timers Pasados"
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -2,22 +2,23 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package. # This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
# Translators: # Translators:
# François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2020 # François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2020
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2020 # Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2020
# Keven D. <theenarki@gmail.com>, 2020 # Keven D. <theenarki@gmail.com>, 2020
# Idea ., 2021 # Idea ., 2021
# Mickael PATTE, 2021 # Mickael PATTE, 2021
# # Geoffrey Fabbro, 2021
#
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Mickael PATTE, 2021\n" "Last-Translator: Geoffrey Fabbro, 2021\n"
"Language-Team: French (France) (https://www.transifex.com/alliance-auth/teams/107430/fr_FR/)\n" "Language-Team: French (France) (https://www.transifex.com/alliance-auth/teams/107430/fr_FR/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -31,7 +32,7 @@ msgstr ""
#: allianceauth/analytics/models.py:30 #: allianceauth/analytics/models.py:30
msgid "Google Analytics V4" msgid "Google Analytics V4"
msgstr "" msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37 #: allianceauth/authentication/decorators.py:37
msgid "A main character is required to perform that action. Add one below." msgid "A main character is required to perform that action. Add one below."
@@ -43,12 +44,12 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "État changé à: %s" msgstr "État changé à: %s"
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "L'état de votre personnage est maintenant: %(state)s" msgstr "L'état de votre personnage est maintenant: %(state)s"
@@ -69,29 +70,29 @@ msgstr ""
"\n" "\n"
" Personnage Principal (État: %(state)s)" " Personnage Principal (État: %(state)s)"
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "Aucun personnage principal sélectionné." msgstr "Aucun personnage principal sélectionné."
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "Ajouter un Personnage" msgstr "Ajouter un Personnage"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "Changer de Personnage Principal" msgstr "Changer de Personnage Principal"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "Groupes" msgstr "Groupes"
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "Personnages" msgstr "Personnages"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -100,13 +101,13 @@ msgstr "Personnages"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "Corpo" msgstr "Corpo"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -119,7 +120,7 @@ msgstr "Connexion"
#: allianceauth/authentication/templates/public/register.html:7 #: allianceauth/authentication/templates/public/register.html:7
msgid "Registration" msgid "Registration"
msgstr "" msgstr "Enregistrement"
#: allianceauth/authentication/templates/public/register.html:22 #: allianceauth/authentication/templates/public/register.html:22
#: allianceauth/authentication/templates/registration/registration_form.html:5 #: allianceauth/authentication/templates/registration/registration_form.html:5
@@ -400,7 +401,7 @@ msgstr "Utilisateur"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -479,7 +480,6 @@ msgstr "Flotte"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -488,8 +488,8 @@ msgstr "Créateur"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "Durée" msgstr "Durée"
@@ -574,11 +574,105 @@ msgstr "Participation à la flotte enregistrée."
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "le lien a expiré" msgstr "le lien a expiré"
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "Gestion de groupe" msgstr "Gestion de groupe"
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -606,7 +700,7 @@ msgstr "Type"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -672,6 +766,7 @@ msgstr "Groupes"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "Description" msgstr "Description"
@@ -868,24 +963,24 @@ msgstr "Vous faites déjà parti de ce groupe."
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "Vous avez déjà une application en attente pour joindre ce groupe." msgstr "Vous avez déjà une application en attente pour joindre ce groupe."
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "Appliqué au groupe %(group)s." msgstr "Appliqué au groupe %(group)s."
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "Vous ne pouvez pas quitter ce groupe." msgstr "Vous ne pouvez pas quitter ce groupe."
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "Vous n'êtes pas un membre de ce groupe." msgstr "Vous n'êtes pas un membre de ce groupe."
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "Vous avec déjà une demande de quitter ce groupe en attente." msgstr "Vous avec déjà une demande de quitter ce groupe en attente."
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "Appliqué pour quitter le groupe %(group)s." msgstr "Appliqué pour quitter le groupe %(group)s."
@@ -924,7 +1019,7 @@ msgstr "Appliquer"
#: allianceauth/hrapplications/templates/hrapplications/management.html:6 #: allianceauth/hrapplications/templates/hrapplications/management.html:6
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:6 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:6
msgid "HR Application Management" msgid "HR Application Management"
msgstr "" msgstr "Gestion de l'application HR"
#: allianceauth/hrapplications/templates/hrapplications/management.html:11 #: allianceauth/hrapplications/templates/hrapplications/management.html:11
msgid "Personal Applications" msgid "Personal Applications"
@@ -1102,7 +1197,7 @@ msgstr "Supprimer tous lu"
#: allianceauth/notifications/templates/notifications/list.html:33 #: allianceauth/notifications/templates/notifications/list.html:33
#: allianceauth/notifications/templates/notifications/list.html:63 #: allianceauth/notifications/templates/notifications/list.html:63
msgid "Timestamp" msgid "Timestamp"
msgstr "" msgstr "Horodatage"
#: allianceauth/notifications/templates/notifications/list.html:34 #: allianceauth/notifications/templates/notifications/list.html:34
#: allianceauth/notifications/templates/notifications/list.html:64 #: allianceauth/notifications/templates/notifications/list.html:64
@@ -1140,49 +1235,62 @@ msgstr "Toutes les notifications ont été marquées comme lues."
#: allianceauth/notifications/views.py:91 #: allianceauth/notifications/views.py:91
msgid "Deleted all read notifications." msgid "Deleted all read notifications."
msgstr "" msgstr "Supprimer toutes les notifications lues"
#: allianceauth/optimer/auth_hooks.py:10 #: allianceauth/optimer/auth_hooks.py:10
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "Opérations de flotte" msgstr "Opérations de flotte"
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "Composition" msgstr "Composition"
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "Heure de départ" msgstr "Heure de départ"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "Nom de l'opération" msgstr "Nom de l'opération"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "Commandant de flotte" msgstr "Commandant de flotte"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Information additionnelle"
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "Créer une opération" msgstr "Créer une opération"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "Système de départ" msgstr "Système de départ"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "Heure Locale" msgstr "Heure Locale"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "Commandant de flotte" msgstr "Commandant de flotte"
@@ -1200,9 +1308,8 @@ msgid "Current Eve Time:"
msgstr "Heure d'Eve actuelle:" msgstr "Heure d'Eve actuelle:"
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers" msgstr ""
msgstr "Prochains minuteurs"
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
#: allianceauth/timerboard/templates/timerboard/view.html:363 #: allianceauth/timerboard/templates/timerboard/view.html:363
@@ -1210,9 +1317,8 @@ msgid "No upcoming timers."
msgstr "Aucun minuteur à venir." msgstr "Aucun minuteur à venir."
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers" msgstr ""
msgstr "Minuteurs précédents"
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:536 #: allianceauth/timerboard/templates/timerboard/view.html:536
@@ -1229,17 +1335,17 @@ msgstr "Mettre à jour l'objectif de la flotte"
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "L'objectif de la flotte n'existe pas." msgstr "L'objectif de la flotte n'existe pas."
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "Minuteur d'opération créé pour %(opname)s." msgstr "Minuteur d'opération créé pour %(opname)s."
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "Minuteur d'opération supprimé pour %(opname)s." msgstr "Minuteur d'opération supprimé pour %(opname)s."
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "Minuteur d'opération modifié pour %(opname)s." msgstr "Minuteur d'opération modifié pour %(opname)s."
@@ -1247,7 +1353,7 @@ msgstr "Minuteur d'opération modifié pour %(opname)s."
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:6 #: allianceauth/permissions_tool/templates/permissions_tool/audit.html:6
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:10 #: allianceauth/permissions_tool/templates/permissions_tool/audit.html:10
msgid "Permissions Audit" msgid "Permissions Audit"
msgstr "" msgstr "Audit des Permissions"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22 #: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22
msgid "User / Character" msgid "User / Character"
@@ -1403,11 +1509,11 @@ msgstr "Mot de passe"
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "Votre mot de passe doit contenir au moins 8 caractères." msgstr "Votre mot de passe doit contenir au moins 8 caractères."
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "Compte Discord Désactivé" msgstr "Compte Discord Désactivé"
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1656,7 +1762,7 @@ msgstr "Une erreur est survenue durant la gestion de votre compte XenForo."
#: allianceauth/services/modules/xenforo/views.py:50 #: allianceauth/services/modules/xenforo/views.py:50
msgid "Deactivated XenForo account." msgid "Deactivated XenForo account."
msgstr "" msgstr "Désactivation du compte XenForo"
#: allianceauth/services/modules/xenforo/views.py:65 #: allianceauth/services/modules/xenforo/views.py:65
msgid "Reset XenForo account password." msgid "Reset XenForo account password."
@@ -1672,7 +1778,7 @@ msgstr "Outil de format de flotte"
#: allianceauth/services/templates/services/fleetformattertool.html:11 #: allianceauth/services/templates/services/fleetformattertool.html:11
msgid "Fleet Broadcast Formatter Tool" msgid "Fleet Broadcast Formatter Tool"
msgstr "" msgstr "Outil de diffusion des flottes formées"
#: allianceauth/services/templates/services/fleetformattertool.html:24 #: allianceauth/services/templates/services/fleetformattertool.html:24
msgid "Format" msgid "Format"
@@ -1714,7 +1820,7 @@ msgstr "Définir le mot de passe"
#: allianceauth/services/templates/services/services.html:5 #: allianceauth/services/templates/services/services.html:5
msgid "Services Management" msgid "Services Management"
msgstr "" msgstr "Gestion de Services"
#: allianceauth/services/templates/services/services.html:10 #: allianceauth/services/templates/services/services.html:10
msgid "Available Services" msgid "Available Services"
@@ -1742,10 +1848,6 @@ msgstr "Heure de flotte"
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "Composition de flotte" msgstr "Composition de flotte"
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Information additionnelle"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "Lien ZkillBoard ()" msgstr "Lien ZkillBoard ()"
@@ -1835,15 +1937,15 @@ msgstr ""
#: allianceauth/srp/templates/srp/data.html:178 #: allianceauth/srp/templates/srp/data.html:178
msgid "No SRP requests for this fleet." msgid "No SRP requests for this fleet."
msgstr "" msgstr "Aucune requête d'SRP pour cette flotte"
#: allianceauth/srp/templates/srp/management.html:8 #: allianceauth/srp/templates/srp/management.html:8
msgid "Srp Management" msgid "Srp Management"
msgstr "" msgstr "Gestion du SRP"
#: allianceauth/srp/templates/srp/management.html:14 #: allianceauth/srp/templates/srp/management.html:14
msgid "SRP Management" msgid "SRP Management"
msgstr "" msgstr "Gestion du SRP"
#: allianceauth/srp/templates/srp/management.html:18 #: allianceauth/srp/templates/srp/management.html:18
msgid "View All" msgid "View All"
@@ -1851,15 +1953,15 @@ msgstr "Afficher Tout"
#: allianceauth/srp/templates/srp/management.html:23 #: allianceauth/srp/templates/srp/management.html:23
msgid "Add SRP Fleet" msgid "Add SRP Fleet"
msgstr "" msgstr "Ajouter une flotte SRP"
#: allianceauth/srp/templates/srp/management.html:41 #: allianceauth/srp/templates/srp/management.html:41
msgid "Fleet AAR" msgid "Fleet AAR"
msgstr "" msgstr "Flotte AAR"
#: allianceauth/srp/templates/srp/management.html:42 #: allianceauth/srp/templates/srp/management.html:42
msgid "Fleet SRP Code" msgid "Fleet SRP Code"
msgstr "" msgstr "Code de la flotte SRP"
#: allianceauth/srp/templates/srp/management.html:43 #: allianceauth/srp/templates/srp/management.html:43
msgid "Fleet ISK Cost" msgid "Fleet ISK Cost"
@@ -1867,11 +1969,11 @@ msgstr "Coût en ISK de la flotte"
#: allianceauth/srp/templates/srp/management.html:44 #: allianceauth/srp/templates/srp/management.html:44
msgid "SRP Status" msgid "SRP Status"
msgstr "" msgstr "Statut du SRP"
#: allianceauth/srp/templates/srp/management.html:45 #: allianceauth/srp/templates/srp/management.html:45
msgid "Pending Requests" msgid "Pending Requests"
msgstr "" msgstr "Requête en Attente"
#: allianceauth/srp/templates/srp/management.html:64 #: allianceauth/srp/templates/srp/management.html:64
msgid "Link" msgid "Link"
@@ -1887,50 +1989,50 @@ msgstr "Complété"
#: allianceauth/srp/templates/srp/management.html:103 #: allianceauth/srp/templates/srp/management.html:103
msgid "Are you sure you want to delete this SRP code and its contents?" msgid "Are you sure you want to delete this SRP code and its contents?"
msgstr "" msgstr "êtes vous sur de vouloir supprimer le code SRP et tout sont contenu"
#: allianceauth/srp/templates/srp/management.html:124 #: allianceauth/srp/templates/srp/management.html:124
msgid "No SRP fleets created." msgid "No SRP fleets created."
msgstr "" msgstr "Aucune flotte de SRP crée"
#: allianceauth/srp/templates/srp/request.html:6 #: allianceauth/srp/templates/srp/request.html:6
msgid "SRP Request" msgid "SRP Request"
msgstr "" msgstr "Requête de SRP"
#: allianceauth/srp/templates/srp/request.html:11 #: allianceauth/srp/templates/srp/request.html:11
#: allianceauth/srp/templates/srp/request.html:20 #: allianceauth/srp/templates/srp/request.html:20
msgid "Create SRP Request" msgid "Create SRP Request"
msgstr "" msgstr "Création de la requête SRP"
#: allianceauth/srp/templates/srp/update.html:6 #: allianceauth/srp/templates/srp/update.html:6
#: allianceauth/srp/templates/srp/update.html:11 #: allianceauth/srp/templates/srp/update.html:11
#: allianceauth/srp/templates/srp/update.html:23 #: allianceauth/srp/templates/srp/update.html:23
msgid "Update AAR Link" msgid "Update AAR Link"
msgstr "" msgstr "Mise à jour du lien AAR"
#: allianceauth/srp/templates/srp/update.html:17 #: allianceauth/srp/templates/srp/update.html:17
msgid "SRP Fleet Does Not Exist" msgid "SRP Fleet Does Not Exist"
msgstr "" msgstr "Aucune flotte SRP existante"
#: allianceauth/srp/views.py:85 #: allianceauth/srp/views.py:85
#, python-format #, python-format
msgid "Created SRP fleet %(fleetname)s." msgid "Created SRP fleet %(fleetname)s."
msgstr "" msgstr "Flotte SRP %(fleetname)s Crée."
#: allianceauth/srp/views.py:103 #: allianceauth/srp/views.py:103
#, python-format #, python-format
msgid "Removed SRP fleet %(fleetname)s." msgid "Removed SRP fleet %(fleetname)s."
msgstr "" msgstr "Flotte SRP %(fleetname)s Supprimée."
#: allianceauth/srp/views.py:115 #: allianceauth/srp/views.py:115
#, python-format #, python-format
msgid "Disabled SRP fleet %(fleetname)s." msgid "Disabled SRP fleet %(fleetname)s."
msgstr "" msgstr "Flotte SRP %(fleetname)sDésactivée."
#: allianceauth/srp/views.py:127 #: allianceauth/srp/views.py:127
#, python-format #, python-format
msgid "Enabled SRP fleet %(fleetname)s." msgid "Enabled SRP fleet %(fleetname)s."
msgstr "" msgstr "Flotte SRP %(fleetname)sActive."
#: allianceauth/srp/views.py:140 #: allianceauth/srp/views.py:140
#, python-format #, python-format
@@ -1949,60 +2051,62 @@ msgstr ""
#: allianceauth/srp/views.py:179 #: allianceauth/srp/views.py:179
msgid "This kill mail has already been posted." msgid "This kill mail has already been posted."
msgstr "" msgstr "Ce Kill Mail a déjà été posté"
#: allianceauth/srp/views.py:200 #: allianceauth/srp/views.py:200
msgid "" msgid ""
"Your SRP request Killmail link is invalid. Please make sure you are using " "Your SRP request Killmail link is invalid. Please make sure you are using "
"zKillboard." "zKillboard."
msgstr "" msgstr ""
"Votre requête SRP ou lien Killmail est invalide. Veuillez vérifier que vous "
"avez bien utilisé zKillboard"
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
"API key for this character and try again" "API key for this character and try again"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "" msgstr "Aucune requête SRP sélectionnée"
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "" msgstr "Impossible à trouver, veuillez sélectionner une autre requête SRP"
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "" msgstr "Suppressions de la requête SRP %(numrequests)s"
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "" msgstr "requête SRP %(numrequests)s Approuvé"
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "" msgstr "Impossible à trouver, veuillez sélectionner une autre requête SRP"
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "" msgstr ""
@@ -2017,11 +2121,11 @@ msgstr "Fermé"
#: allianceauth/templates/allianceauth/admin-status/overview.html:26 #: allianceauth/templates/allianceauth/admin-status/overview.html:26
msgid "Powered by GitLab" msgid "Powered by GitLab"
msgstr "" msgstr "Propulsé par Gitlab"
#: allianceauth/templates/allianceauth/admin-status/overview.html:33 #: allianceauth/templates/allianceauth/admin-status/overview.html:33
msgid "Support Discord" msgid "Support Discord"
msgstr "" msgstr "Support Discord"
#: allianceauth/templates/allianceauth/admin-status/overview.html:41 #: allianceauth/templates/allianceauth/admin-status/overview.html:41
msgid "Software Version" msgid "Software Version"
@@ -2041,11 +2145,11 @@ msgstr "Mise à jour disponible"
#: allianceauth/templates/allianceauth/admin-status/overview.html:62 #: allianceauth/templates/allianceauth/admin-status/overview.html:62
msgid "Latest Pre-Release" msgid "Latest Pre-Release"
msgstr "" msgstr "Dernière Pre-Release"
#: allianceauth/templates/allianceauth/admin-status/overview.html:68 #: allianceauth/templates/allianceauth/admin-status/overview.html:68
msgid "Pre-Release available" msgid "Pre-Release available"
msgstr "" msgstr "Pre-Release disponible"
#: allianceauth/templates/allianceauth/admin-status/overview.html:76 #: allianceauth/templates/allianceauth/admin-status/overview.html:76
msgid "Task Queue" msgid "Task Queue"
@@ -2074,11 +2178,11 @@ msgstr "Administrateur"
#: allianceauth/templates/allianceauth/top-menu-admin.html:19 #: allianceauth/templates/allianceauth/top-menu-admin.html:19
msgid "AA Documentation" msgid "AA Documentation"
msgstr "" msgstr "Documentation AA"
#: allianceauth/templates/allianceauth/top-menu-admin.html:26 #: allianceauth/templates/allianceauth/top-menu-admin.html:26
msgid "AA Support Discord" msgid "AA Support Discord"
msgstr "" msgstr "Support Discord AA"
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:10 #: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:10
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:14 #: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:14
@@ -2231,6 +2335,14 @@ msgstr "Minuteur de corporation"
msgid "Structure" msgid "Structure"
msgstr "Structure" msgstr "Structure"
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr "Prochains minuteurs"
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr "Minuteurs précédents"
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -4,17 +4,17 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
# Translators: # Translators:
# Linus Hope, 2021
# Alessandro Cresti, 2021 # Alessandro Cresti, 2021
# Linus Hope, 2021
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Alessandro Cresti, 2021\n" "Last-Translator: Linus Hope, 2021\n"
"Language-Team: Italian (Italy) (https://www.transifex.com/alliance-auth/teams/107430/it_IT/)\n" "Language-Team: Italian (Italy) (https://www.transifex.com/alliance-auth/teams/107430/it_IT/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -40,12 +40,12 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "" msgstr ""
@@ -67,29 +67,29 @@ msgstr ""
" Personaggio principale (State: %(state)s)\n" " Personaggio principale (State: %(state)s)\n"
" " " "
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "Nessun personaggio principale impostato" msgstr "Nessun personaggio principale impostato"
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "Aggiungi personaggio" msgstr "Aggiungi personaggio"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "Cambia personaggio principale" msgstr "Cambia personaggio principale"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "Gruppi dei quali fai parte" msgstr "Gruppi dei quali fai parte"
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "Personaggi" msgstr "Personaggi"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -98,13 +98,13 @@ msgstr "Personaggi"
msgid "Name" msgid "Name"
msgstr "Nome" msgstr "Nome"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "Corporazione" msgstr "Corporazione"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -117,7 +117,7 @@ msgstr ""
#: allianceauth/authentication/templates/public/register.html:7 #: allianceauth/authentication/templates/public/register.html:7
msgid "Registration" msgid "Registration"
msgstr "" msgstr "Iscriviti"
#: allianceauth/authentication/templates/public/register.html:22 #: allianceauth/authentication/templates/public/register.html:22
#: allianceauth/authentication/templates/registration/registration_form.html:5 #: allianceauth/authentication/templates/registration/registration_form.html:5
@@ -217,7 +217,7 @@ msgstr "Statistiche della corporazione"
#: allianceauth/corputils/templates/corputils/base.html:3 #: allianceauth/corputils/templates/corputils/base.html:3
#: allianceauth/corputils/templates/corputils/base.html:6 #: allianceauth/corputils/templates/corputils/base.html:6
msgid "Corporation Member Data" msgid "Corporation Member Data"
msgstr "" msgstr "Informazioni sui membri della corporazione"
#: allianceauth/corputils/templates/corputils/base.html:12 #: allianceauth/corputils/templates/corputils/base.html:12
msgid "Corporations" msgid "Corporations"
@@ -401,7 +401,7 @@ msgstr "Utente"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -480,7 +480,6 @@ msgstr "Flotta"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -489,8 +488,8 @@ msgstr "Autore"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "Durata" msgstr "Durata"
@@ -575,11 +574,105 @@ msgstr "Partecipazione alla flotta registrata."
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "Il FAT link è scaduto." msgstr "Il FAT link è scaduto."
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "" msgstr ""
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -607,7 +700,7 @@ msgstr "Tipo"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -673,6 +766,7 @@ msgstr "Gruppi"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "Descrizione" msgstr "Descrizione"
@@ -806,20 +900,20 @@ msgstr ""
#: allianceauth/groupmanagement/views.py:159 #: allianceauth/groupmanagement/views.py:159
#, python-format #, python-format
msgid "Removed user %(user)s from group %(group)s." msgid "Removed user %(user)s from group %(group)s."
msgstr "" msgstr "Rimosso il membro %(user)s da %(group)s."
#: allianceauth/groupmanagement/views.py:161 #: allianceauth/groupmanagement/views.py:161
msgid "User does not exist in that group" msgid "User does not exist in that group"
msgstr "" msgstr "Lutente non fa parte del gruppo selezionato"
#: allianceauth/groupmanagement/views.py:164 #: allianceauth/groupmanagement/views.py:164
msgid "Group does not exist" msgid "Group does not exist"
msgstr "" msgstr "Il gruppo non esiste"
#: allianceauth/groupmanagement/views.py:191 #: allianceauth/groupmanagement/views.py:191
#, python-format #, python-format
msgid "Accepted application from %(mainchar)s to %(group)s." msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "" msgstr "La domanda di %(mainchar)s per %(group)s è stata accettata."
#: allianceauth/groupmanagement/views.py:197 #: allianceauth/groupmanagement/views.py:197
#: allianceauth/groupmanagement/views.py:228 #: allianceauth/groupmanagement/views.py:228
@@ -828,16 +922,20 @@ msgid ""
"An unhandled error occurred while processing the application from " "An unhandled error occurred while processing the application from "
"%(mainchar)s to %(group)s." "%(mainchar)s to %(group)s."
msgstr "" msgstr ""
"Si è verificato unerrore durante lelaborazione della domanda di "
"%(mainchar)s per %(group)s."
#: allianceauth/groupmanagement/views.py:222 #: allianceauth/groupmanagement/views.py:222
#, python-format #, python-format
msgid "Rejected application from %(mainchar)s to %(group)s." msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "" msgstr "La domanda di %(mainchar)s per %(group)s è stata rifiutata."
#: allianceauth/groupmanagement/views.py:257 #: allianceauth/groupmanagement/views.py:257
#, python-format #, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s." msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "" msgstr ""
"La domanda di congedo da parte di %(mainchar)s per %(group)s è stata "
"accettata."
#: allianceauth/groupmanagement/views.py:262 #: allianceauth/groupmanagement/views.py:262
#: allianceauth/groupmanagement/views.py:294 #: allianceauth/groupmanagement/views.py:294
@@ -846,91 +944,95 @@ msgid ""
"An unhandled error occurred while processing the application from " "An unhandled error occurred while processing the application from "
"%(mainchar)s to leave %(group)s." "%(mainchar)s to leave %(group)s."
msgstr "" msgstr ""
"Si è verificato unerrore durante lelaborazione della domanda di comgedo da"
" parte di %(mainchar)s per %(group)s."
#: allianceauth/groupmanagement/views.py:288 #: allianceauth/groupmanagement/views.py:288
#, python-format #, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s." msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "" msgstr ""
"La domanda di congedo da parte di %(mainchar)s per %(group)s è stata "
"rifiutata."
#: allianceauth/groupmanagement/views.py:332 #: allianceauth/groupmanagement/views.py:332
#: allianceauth/groupmanagement/views.py:342 #: allianceauth/groupmanagement/views.py:342
msgid "You cannot join that group" msgid "You cannot join that group"
msgstr "" msgstr "Non puoi aderire a questo gruppo"
#: allianceauth/groupmanagement/views.py:337 #: allianceauth/groupmanagement/views.py:337
msgid "You are already a member of that group." msgid "You are already a member of that group."
msgstr "" msgstr "Sei già parte del gruppo selezionato."
#: allianceauth/groupmanagement/views.py:354 #: allianceauth/groupmanagement/views.py:354
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "" msgstr "La tua domanda per questo gruppo non è ancora stata valutata."
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "" msgstr "Hai fatto domanda per il gruppo %(group)s."
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "" msgstr "Non puoi lasciare questo gruppo."
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "" msgstr "Non sei un membro di questo gruppo."
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "" msgstr "La tua domanda di congedo non è ancora stata valutata."
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "" msgstr "Hai fatto domanda di congedo per %(group)s."
#: allianceauth/hrapplications/auth_hooks.py:14 #: allianceauth/hrapplications/auth_hooks.py:14
msgid "Applications" msgid "Applications"
msgstr "" msgstr "Domande"
#: allianceauth/hrapplications/forms.py:6 #: allianceauth/hrapplications/forms.py:6
#: allianceauth/hrapplications/templates/hrapplications/view.html:92 #: allianceauth/hrapplications/templates/hrapplications/view.html:92
msgid "Comment" msgid "Comment"
msgstr "" msgstr "Commenti"
#: allianceauth/hrapplications/forms.py:10 #: allianceauth/hrapplications/forms.py:10
msgid "Search String" msgid "Search String"
msgstr "" msgstr "Stringa di ricerca"
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:5 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:5
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:8 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:8
msgid "Choose a Corp" msgid "Choose a Corp"
msgstr "" msgstr "Seleziona una corporazione"
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:11 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:11
msgid "Available Corps" msgid "Available Corps"
msgstr "" msgstr "Corporazioni disponibili"
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:23 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:23
msgid "No corps are accepting applications at this time." msgid "No corps are accepting applications at this time."
msgstr "" msgstr "Nessuna corporazione accetta domanda al momento."
#: allianceauth/hrapplications/templates/hrapplications/create.html:5 #: allianceauth/hrapplications/templates/hrapplications/create.html:5
#: allianceauth/hrapplications/templates/hrapplications/create.html:8 #: allianceauth/hrapplications/templates/hrapplications/create.html:8
msgid "Apply To" msgid "Apply To"
msgstr "" msgstr "Applica a"
#: allianceauth/hrapplications/templates/hrapplications/management.html:6 #: allianceauth/hrapplications/templates/hrapplications/management.html:6
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:6 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:6
msgid "HR Application Management" msgid "HR Application Management"
msgstr "" msgstr "HR Risorse umane"
#: allianceauth/hrapplications/templates/hrapplications/management.html:11 #: allianceauth/hrapplications/templates/hrapplications/management.html:11
msgid "Personal Applications" msgid "Personal Applications"
msgstr "" msgstr "Domande personali"
#: allianceauth/hrapplications/templates/hrapplications/management.html:15 #: allianceauth/hrapplications/templates/hrapplications/management.html:15
#: allianceauth/hrapplications/templates/hrapplications/management.html:18 #: allianceauth/hrapplications/templates/hrapplications/management.html:18
msgid "Create Application" msgid "Create Application"
msgstr "" msgstr "Crea una domanda"
#: allianceauth/hrapplications/templates/hrapplications/management.html:26 #: allianceauth/hrapplications/templates/hrapplications/management.html:26
#: allianceauth/hrapplications/templates/hrapplications/management.html:80 #: allianceauth/hrapplications/templates/hrapplications/management.html:80
@@ -938,7 +1040,7 @@ msgstr ""
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:24 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:24
#: allianceauth/services/templates/services/services.html:16 #: allianceauth/services/templates/services/services.html:16
msgid "Username" msgid "Username"
msgstr "" msgstr "Nome utente"
#: allianceauth/hrapplications/templates/hrapplications/management.html:29 #: allianceauth/hrapplications/templates/hrapplications/management.html:29
#: allianceauth/hrapplications/templates/hrapplications/management.html:84 #: allianceauth/hrapplications/templates/hrapplications/management.html:84
@@ -948,7 +1050,7 @@ msgstr ""
#: allianceauth/srp/templates/srp/data.html:103 #: allianceauth/srp/templates/srp/data.html:103
#: allianceauth/srp/templates/srp/management.html:46 #: allianceauth/srp/templates/srp/management.html:46
msgid "Actions" msgid "Actions"
msgstr "" msgstr "Azioni"
#: allianceauth/hrapplications/templates/hrapplications/management.html:39 #: allianceauth/hrapplications/templates/hrapplications/management.html:39
#: allianceauth/hrapplications/templates/hrapplications/management.html:100 #: allianceauth/hrapplications/templates/hrapplications/management.html:100
@@ -957,7 +1059,7 @@ msgstr ""
#: allianceauth/hrapplications/templates/hrapplications/view.html:16 #: allianceauth/hrapplications/templates/hrapplications/view.html:16
#: allianceauth/srp/templates/srp/data.html:130 #: allianceauth/srp/templates/srp/data.html:130
msgid "Approved" msgid "Approved"
msgstr "" msgstr "Approvato"
#: allianceauth/hrapplications/templates/hrapplications/management.html:41 #: allianceauth/hrapplications/templates/hrapplications/management.html:41
#: allianceauth/hrapplications/templates/hrapplications/management.html:102 #: allianceauth/hrapplications/templates/hrapplications/management.html:102
@@ -965,25 +1067,25 @@ msgstr ""
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:42 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:42
#: allianceauth/srp/templates/srp/data.html:134 #: allianceauth/srp/templates/srp/data.html:134
msgid "Rejected" msgid "Rejected"
msgstr "" msgstr "Rifiutato"
#: allianceauth/hrapplications/templates/hrapplications/management.html:61 #: allianceauth/hrapplications/templates/hrapplications/management.html:61
msgid "Application Management" msgid "Application Management"
msgstr "" msgstr "Gestione delle domande"
#: allianceauth/hrapplications/templates/hrapplications/management.html:65 #: allianceauth/hrapplications/templates/hrapplications/management.html:65
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:16 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:16
msgid "Search Applications" msgid "Search Applications"
msgstr "" msgstr "Cerca domande"
#: allianceauth/hrapplications/templates/hrapplications/management.html:71 #: allianceauth/hrapplications/templates/hrapplications/management.html:71
msgid "Reviewed" msgid "Reviewed"
msgstr "" msgstr "Revisionato"
#: allianceauth/hrapplications/templates/hrapplications/management.html:79 #: allianceauth/hrapplications/templates/hrapplications/management.html:79
#: allianceauth/hrapplications/templates/hrapplications/management.html:123 #: allianceauth/hrapplications/templates/hrapplications/management.html:123
msgid "Date" msgid "Date"
msgstr "" msgstr "Data"
#: allianceauth/hrapplications/templates/hrapplications/management.html:95 #: allianceauth/hrapplications/templates/hrapplications/management.html:95
#: allianceauth/hrapplications/templates/hrapplications/management.html:139 #: allianceauth/hrapplications/templates/hrapplications/management.html:139
@@ -993,126 +1095,126 @@ msgstr ""
#: allianceauth/hrapplications/templates/hrapplications/management.html:114 #: allianceauth/hrapplications/templates/hrapplications/management.html:114
msgid "No pending applications." msgid "No pending applications."
msgstr "" msgstr "Nessuna domanda in sospeso."
#: allianceauth/hrapplications/templates/hrapplications/management.html:163 #: allianceauth/hrapplications/templates/hrapplications/management.html:163
msgid "No reviewed applications." msgid "No reviewed applications."
msgstr "" msgstr "Nessuna domanda revisionata."
#: allianceauth/hrapplications/templates/hrapplications/management.html:177 #: allianceauth/hrapplications/templates/hrapplications/management.html:177
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:63 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:63
#: allianceauth/hrapplications/templates/hrapplications/view.html:135 #: allianceauth/hrapplications/templates/hrapplications/view.html:135
msgid "Close" msgid "Close"
msgstr "" msgstr "Chiudi"
#: allianceauth/hrapplications/templates/hrapplications/management.html:178 #: allianceauth/hrapplications/templates/hrapplications/management.html:178
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:64 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:64
msgid "Application Search" msgid "Application Search"
msgstr "" msgstr "Cerca domande"
#: allianceauth/hrapplications/templates/hrapplications/management.html:185 #: allianceauth/hrapplications/templates/hrapplications/management.html:185
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:71 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:71
msgid "Search" msgid "Search"
msgstr "" msgstr "Cerca"
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:12 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:12
msgid "Application Search Results" msgid "Application Search Results"
msgstr "" msgstr "Risultati della tua ricerca domande"
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:23 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:23
msgid "Application ID" msgid "Application ID"
msgstr "" msgstr "ID Domande"
#: allianceauth/hrapplications/templates/hrapplications/view.html:6 #: allianceauth/hrapplications/templates/hrapplications/view.html:6
#: allianceauth/hrapplications/templates/hrapplications/view.html:11 #: allianceauth/hrapplications/templates/hrapplications/view.html:11
msgid "View Application" msgid "View Application"
msgstr "" msgstr "Visiona domanda"
#: allianceauth/hrapplications/templates/hrapplications/view.html:18 #: allianceauth/hrapplications/templates/hrapplications/view.html:18
msgid "Denied" msgid "Denied"
msgstr "" msgstr "Accesso negato"
#: allianceauth/hrapplications/templates/hrapplications/view.html:28 #: allianceauth/hrapplications/templates/hrapplications/view.html:28
msgid "Applicant" msgid "Applicant"
msgstr "" msgstr "Candidato"
#: allianceauth/hrapplications/templates/hrapplications/view.html:79 #: allianceauth/hrapplications/templates/hrapplications/view.html:79
msgid "Approve" msgid "Approve"
msgstr "" msgstr "Approva"
#: allianceauth/hrapplications/templates/hrapplications/view.html:85 #: allianceauth/hrapplications/templates/hrapplications/view.html:85
msgid "Delete" msgid "Delete"
msgstr "" msgstr "Cancella"
#: allianceauth/hrapplications/templates/hrapplications/view.html:88 #: allianceauth/hrapplications/templates/hrapplications/view.html:88
msgid "Mark in Progress" msgid "Mark in Progress"
msgstr "" msgstr "Segnala in elaborazione"
#: allianceauth/hrapplications/templates/hrapplications/view.html:102 #: allianceauth/hrapplications/templates/hrapplications/view.html:102
#: allianceauth/services/forms.py:17 #: allianceauth/services/forms.py:17
msgid "Comments" msgid "Comments"
msgstr "" msgstr "Commenti"
#: allianceauth/hrapplications/templates/hrapplications/view.html:137 #: allianceauth/hrapplications/templates/hrapplications/view.html:137
#: allianceauth/hrapplications/templates/hrapplications/view.html:144 #: allianceauth/hrapplications/templates/hrapplications/view.html:144
msgid "Add Comment" msgid "Add Comment"
msgstr "" msgstr "Aggiungi commento"
#: allianceauth/notifications/models.py:21 #: allianceauth/notifications/models.py:21
msgid "danger" msgid "danger"
msgstr "" msgstr "pericolo"
#: allianceauth/notifications/models.py:22 #: allianceauth/notifications/models.py:22
msgid "warning" msgid "warning"
msgstr "" msgstr "attenzione"
#: allianceauth/notifications/models.py:23 #: allianceauth/notifications/models.py:23
msgid "info" msgid "info"
msgstr "" msgstr "informazioni"
#: allianceauth/notifications/models.py:24 #: allianceauth/notifications/models.py:24
msgid "success" msgid "success"
msgstr "" msgstr "successo"
#: allianceauth/notifications/templates/notifications/list.html:5 #: allianceauth/notifications/templates/notifications/list.html:5
#: allianceauth/notifications/templates/notifications/list.html:9 #: allianceauth/notifications/templates/notifications/list.html:9
#: allianceauth/templates/allianceauth/notifications_menu_item.html:6 #: allianceauth/templates/allianceauth/notifications_menu_item.html:6
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr "Notifiche"
#: allianceauth/notifications/templates/notifications/list.html:16 #: allianceauth/notifications/templates/notifications/list.html:16
msgid "Unread" msgid "Unread"
msgstr "" msgstr "Non letto"
#: allianceauth/notifications/templates/notifications/list.html:18 #: allianceauth/notifications/templates/notifications/list.html:18
msgid "Read" msgid "Read"
msgstr "" msgstr "Letto"
#: allianceauth/notifications/templates/notifications/list.html:21 #: allianceauth/notifications/templates/notifications/list.html:21
msgid "Mark All Read" msgid "Mark All Read"
msgstr "" msgstr "Seleziona tutto visionato"
#: allianceauth/notifications/templates/notifications/list.html:22 #: allianceauth/notifications/templates/notifications/list.html:22
msgid "Delete All Read" msgid "Delete All Read"
msgstr "" msgstr "Cancella tutti i visionati"
#: allianceauth/notifications/templates/notifications/list.html:33 #: allianceauth/notifications/templates/notifications/list.html:33
#: allianceauth/notifications/templates/notifications/list.html:63 #: allianceauth/notifications/templates/notifications/list.html:63
msgid "Timestamp" msgid "Timestamp"
msgstr "" msgstr "Ora"
#: allianceauth/notifications/templates/notifications/list.html:34 #: allianceauth/notifications/templates/notifications/list.html:34
#: allianceauth/notifications/templates/notifications/list.html:64 #: allianceauth/notifications/templates/notifications/list.html:64
msgid "Title" msgid "Title"
msgstr "" msgstr "Titolo"
#: allianceauth/notifications/templates/notifications/list.html:53 #: allianceauth/notifications/templates/notifications/list.html:53
msgid "No unread notifications." msgid "No unread notifications."
msgstr "" msgstr "Nessuna notifica non visionata."
#: allianceauth/notifications/templates/notifications/list.html:83 #: allianceauth/notifications/templates/notifications/list.html:83
msgid "No read notifications." msgid "No read notifications."
msgstr "" msgstr "Nessuna notifica visionata."
#: allianceauth/notifications/templates/notifications/view.html:5 #: allianceauth/notifications/templates/notifications/view.html:5
#: allianceauth/notifications/templates/notifications/view.html:11 #: allianceauth/notifications/templates/notifications/view.html:11
@@ -1143,43 +1245,56 @@ msgstr ""
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr ""
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "" msgstr ""
@@ -1197,8 +1312,7 @@ msgid "Current Eve Time:"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
@@ -1207,8 +1321,7 @@ msgid "No upcoming timers."
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
@@ -1226,17 +1339,17 @@ msgstr ""
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "" msgstr ""
@@ -1400,11 +1513,11 @@ msgstr ""
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1729,10 +1842,6 @@ msgstr ""
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "" msgstr ""
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr ""
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1942,52 +2051,52 @@ msgid ""
"zKillboard." "zKillboard."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
"API key for this character and try again" "API key for this character and try again"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "" msgstr ""
@@ -2214,6 +2323,14 @@ msgstr ""
msgid "Structure" msgid "Structure"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr ""
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n" "Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n"
"Language-Team: Japanese (https://www.transifex.com/alliance-auth/teams/107430/ja/)\n" "Language-Team: Japanese (https://www.transifex.com/alliance-auth/teams/107430/ja/)\n"
@@ -38,12 +38,12 @@ msgstr "実行するためにはメインキャラクターの設定が必要で
msgid "Email" msgid "Email"
msgstr "メールアドレス" msgstr "メールアドレス"
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "分類が%sに変更されました。" msgstr "分類が%sに変更されました。"
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "あなたの分類は%(state)sになりました。" msgstr "あなたの分類は%(state)sになりました。"
@@ -64,29 +64,29 @@ msgstr ""
"\n" "\n"
" メインキャラクター(分類:%(state)s" " メインキャラクター(分類:%(state)s"
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "メンキャラクターが選択されていません。" msgstr "メンキャラクターが選択されていません。"
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "キャラクターを追加" msgstr "キャラクターを追加"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "メンキャラクターを変更" msgstr "メンキャラクターを変更"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "" msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "キャラクター" msgstr "キャラクター"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -95,13 +95,13 @@ msgstr "キャラクター"
msgid "Name" msgid "Name"
msgstr "名前" msgstr "名前"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "Corp" msgstr "Corp"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -386,7 +386,7 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -463,7 +463,6 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -472,8 +471,8 @@ msgstr "作成者"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "有効時間" msgstr "有効時間"
@@ -558,11 +557,105 @@ msgstr "Fleet参加が登録されました。"
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "Fat-Linkの有効期間が終了してます。" msgstr "Fat-Linkの有効期間が終了してます。"
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "" msgstr ""
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -590,7 +683,7 @@ msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -656,6 +749,7 @@ msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "" msgstr ""
@@ -848,24 +942,24 @@ msgstr "すでにその Group に参加してます。"
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "すでに参加申請を送付済みです。" msgstr "すでに参加申請を送付済みです。"
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "%(group)sへの参加申請を送信しました。" msgstr "%(group)sへの参加申請を送信しました。"
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "この Group から脱退することはできません" msgstr "この Group から脱退することはできません"
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "あなたはその Group のメンバーではありません" msgstr "あなたはその Group のメンバーではありません"
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "すでに脱退申請を送信済みです。" msgstr "すでに脱退申請を送信済みです。"
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "%(group)sからの脱退申請を送信しました。" msgstr "%(group)sからの脱退申請を送信しました。"
@@ -1126,43 +1220,56 @@ msgstr "確認済みのすべての通知を削除"
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "開始時間" msgstr "開始時間"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "作戦名" msgstr "作戦名"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "" msgstr ""
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr ""
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "" msgstr ""
@@ -1180,8 +1287,7 @@ msgid "Current Eve Time:"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
@@ -1190,8 +1296,7 @@ msgid "No upcoming timers."
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers"
msgstr "" msgstr ""
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
@@ -1209,17 +1314,17 @@ msgstr ""
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "" msgstr ""
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "%(opname)sのTimerが作成されました。" msgstr "%(opname)sのTimerが作成されました。"
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "%(opname)sのTimerが削除されました。" msgstr "%(opname)sのTimerが削除されました。"
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "%(opname)sのTimerの変更が保存されました。" msgstr "%(opname)sのTimerの変更が保存されました。"
@@ -1383,11 +1488,11 @@ msgstr ""
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "Passwordは8 文字以上必要です。" msgstr "Passwordは8 文字以上必要です。"
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "Discordのアカウントを無効化" msgstr "Discordのアカウントを無効化"
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1712,10 +1817,6 @@ msgstr ""
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "" msgstr ""
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr ""
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1928,52 +2029,52 @@ msgid ""
"zKillboard." "zKillboard."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
"API key for this character and try again" "API key for this character and try again"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "" msgstr ""
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "" msgstr ""
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "" msgstr ""
@@ -2199,6 +2300,14 @@ msgstr ""
msgid "Structure" msgid "Structure"
msgstr "" msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr ""
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr ""
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n" "Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n"
"Language-Team: Korean (Korea) (https://www.transifex.com/alliance-auth/teams/107430/ko_KR/)\n" "Language-Team: Korean (Korea) (https://www.transifex.com/alliance-auth/teams/107430/ko_KR/)\n"
@@ -41,12 +41,12 @@ msgstr "해당 기능을 수행하려면 주 캐릭터가 요구됨. 아래에
msgid "Email" msgid "Email"
msgstr "이메일" msgstr "이메일"
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "상태가 %s로 변경됐습니다." msgstr "상태가 %s로 변경됐습니다."
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "사용자의 상태는 %(state)s입니다." msgstr "사용자의 상태는 %(state)s입니다."
@@ -68,29 +68,29 @@ msgstr ""
" 메인 캐릭터 (상태: %(state)s)\n" " 메인 캐릭터 (상태: %(state)s)\n"
" " " "
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "주 캐릭터가 지정되지 않음" msgstr "주 캐릭터가 지정되지 않음"
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "캐릭터 추가" msgstr "캐릭터 추가"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "주 캐릭터 변경" msgstr "주 캐릭터 변경"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "그룹 멤버쉽" msgstr "그룹 멤버쉽"
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "캐릭터" msgstr "캐릭터"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -99,13 +99,13 @@ msgstr "캐릭터"
msgid "Name" msgid "Name"
msgstr "이름" msgstr "이름"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "콥" msgstr "콥"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -390,7 +390,7 @@ msgstr "유저"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -467,7 +467,6 @@ msgstr "함대"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -476,8 +475,8 @@ msgstr "생성자"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "소요 시간" msgstr "소요 시간"
@@ -562,11 +561,105 @@ msgstr "플릿 참여 등록됨"
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "플릿활동추적 링크 기한만료" msgstr "플릿활동추적 링크 기한만료"
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "그룹 관리" msgstr "그룹 관리"
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -594,7 +687,7 @@ msgstr "타입"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -660,6 +753,7 @@ msgstr "그룹"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "설명" msgstr "설명"
@@ -852,24 +946,24 @@ msgstr "이미 해당 그룹에 가입되어 있습니다."
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "해당 그룹에 대한 참여신청이 보류되었습니다." msgstr "해당 그룹에 대한 참여신청이 보류되었습니다."
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "%(group)s그룹에 지원하였음." msgstr "%(group)s그룹에 지원하였음."
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "해당 그룹을 떠날 수 없습니다." msgstr "해당 그룹을 떠날 수 없습니다."
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "해당그룹의 멤버가 아닙니다." msgstr "해당그룹의 멤버가 아닙니다."
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "해당 그룹의 탈퇴 신청이 접수된 상태입니다." msgstr "해당 그룹의 탈퇴 신청이 접수된 상태입니다."
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "%(group)s그룹의 탈퇴가 신청됨." msgstr "%(group)s그룹의 탈퇴가 신청됨."
@@ -1130,43 +1224,56 @@ msgstr "모든 읽은 알림을 삭제했습니다."
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "플릿 옵" msgstr "플릿 옵"
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "독트린" msgstr "독트린"
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "시작 시간" msgstr "시작 시간"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "옵 이름" msgstr "옵 이름"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "플릿 커맨더" msgstr "플릿 커맨더"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "추가 기재 사항"
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "옵 만들기" msgstr "옵 만들기"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "폼업 성계" msgstr "폼업 성계"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "현지 시간" msgstr "현지 시간"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "FC" msgstr "FC"
@@ -1184,9 +1291,8 @@ msgid "Current Eve Time:"
msgstr "현재 이브 시간:" msgstr "현재 이브 시간:"
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers" msgstr ""
msgstr "다음 옵 타이머"
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
#: allianceauth/timerboard/templates/timerboard/view.html:363 #: allianceauth/timerboard/templates/timerboard/view.html:363
@@ -1194,9 +1300,8 @@ msgid "No upcoming timers."
msgstr "예정된 옵 타이머가 없습니다." msgstr "예정된 옵 타이머가 없습니다."
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers" msgstr ""
msgstr "이전 옵 타이머"
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:536 #: allianceauth/timerboard/templates/timerboard/view.html:536
@@ -1213,17 +1318,17 @@ msgstr "플릿 옵 수정"
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "존재하지 않는 플릿 옵" msgstr "존재하지 않는 플릿 옵"
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "%(opname)s 의 옵 타이머를 생성했습니다." msgstr "%(opname)s 의 옵 타이머를 생성했습니다."
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "%(opname)s 의 옵 타이머를 제거했습니다." msgstr "%(opname)s 의 옵 타이머를 제거했습니다."
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "%(opname)s 의 옵 타이머 변경사항을 저장했습니다." msgstr "%(opname)s 의 옵 타이머 변경사항을 저장했습니다."
@@ -1387,11 +1492,11 @@ msgstr "비밀번호"
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "비밀번호는 8글자 이상이어야 합니다." msgstr "비밀번호는 8글자 이상이어야 합니다."
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "디스코드 계정 비활성화" msgstr "디스코드 계정 비활성화"
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1716,10 +1821,6 @@ msgstr "플릿 시간"
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "플릿 독트린" msgstr "플릿 독트린"
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "추가 기재 사항"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1929,12 +2030,12 @@ msgid ""
"zKillboard." "zKillboard."
msgstr "SRP 보상 요구를 위한 킬메일 링크가 유효하지 않습니다. zkillboard를 사용해 주십시요." msgstr "SRP 보상 요구를 위한 킬메일 링크가 유효하지 않습니다. zkillboard를 사용해 주십시요."
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "%(ship)s에 대한 SRP 보상 요청이 제출되었습니다." msgstr "%(ship)s에 대한 SRP 보상 요청이 제출되었습니다."
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
@@ -1942,40 +2043,40 @@ msgid ""
msgstr "" msgstr ""
"%(charid)s 캐릭터가 Auth 계정에 포함되어 있지 않습니다. 해당 캐릭터의 API를 추가하신 후, 다시 시도하시기 바랍니다." "%(charid)s 캐릭터가 Auth 계정에 포함되어 있지 않습니다. 해당 캐릭터의 API를 추가하신 후, 다시 시도하시기 바랍니다."
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "SRP 보상 요청이 선택되지 않았습니다." msgstr "SRP 보상 요청이 선택되지 않았습니다."
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "선택하신 SRP 보상 요청을 찾을 수 없습니다." msgstr "선택하신 SRP 보상 요청을 찾을 수 없습니다."
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "SRP 보상 요청 %(numrequests)s 삭제 완료" msgstr "SRP 보상 요청 %(numrequests)s 삭제 완료"
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "SRP 보상 요청 %(numrequests)s 승인 완료" msgstr "SRP 보상 요청 %(numrequests)s 승인 완료"
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "선택하신 SRP 보상 요청을 찾을 수 없습니다." msgstr "선택하신 SRP 보상 요청을 찾을 수 없습니다."
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "SRP 보상 요청 %(numrequests)s 거절됨." msgstr "SRP 보상 요청 %(numrequests)s 거절됨."
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "SRP 보상 요청 %(requestid)s을 찾을 수 없습니다. " msgstr "SRP 보상 요청 %(requestid)s을 찾을 수 없습니다. "
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "SRP 보상 요청 플릿 %(fleetname)s의 변경 사항이 저장되었습니다." msgstr "SRP 보상 요청 플릿 %(fleetname)s의 변경 사항이 저장되었습니다."
@@ -2201,6 +2302,14 @@ msgstr "콥 타이머"
msgid "Structure" msgid "Structure"
msgstr "스트럭처" msgstr "스트럭처"
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr "다음 옵 타이머"
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr "이전 옵 타이머"
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-26 18:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Андрей Зубков <and.vareba81@gmail.com>, 2020\n" "Last-Translator: Андрей Зубков <and.vareba81@gmail.com>, 2020\n"
"Language-Team: Russian (https://www.transifex.com/alliance-auth/teams/107430/ru/)\n" "Language-Team: Russian (https://www.transifex.com/alliance-auth/teams/107430/ru/)\n"
@@ -39,12 +39,12 @@ msgstr "Необходимо указать основного персонаж
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:79
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "Статус изменен: %s" msgstr "Статус изменен: %s"
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:80
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "Статус пилота: %(state)s" msgstr "Статус пилота: %(state)s"
@@ -66,29 +66,29 @@ msgstr ""
" Основной персонаж (статус: %(state)s)\n" " Основной персонаж (статус: %(state)s)\n"
" " " "
#: allianceauth/authentication/templates/authentication/dashboard.html:81 #: allianceauth/authentication/templates/authentication/dashboard.html:102
msgid "No main character set." msgid "No main character set."
msgstr "Основной персонаж не установлен." msgstr "Основной персонаж не установлен."
#: allianceauth/authentication/templates/authentication/dashboard.html:88 #: allianceauth/authentication/templates/authentication/dashboard.html:109
msgid "Add Character" msgid "Add Character"
msgstr "Добавить Персонажа" msgstr "Добавить Персонажа"
#: allianceauth/authentication/templates/authentication/dashboard.html:92 #: allianceauth/authentication/templates/authentication/dashboard.html:113
msgid "Change Main" msgid "Change Main"
msgstr "Сменить основного персонажа" msgstr "Сменить основного персонажа"
#: allianceauth/authentication/templates/authentication/dashboard.html:101 #: allianceauth/authentication/templates/authentication/dashboard.html:122
msgid "Group Memberships" msgid "Group Memberships"
msgstr "Роли" msgstr "Роли"
#: allianceauth/authentication/templates/authentication/dashboard.html:121 #: allianceauth/authentication/templates/authentication/dashboard.html:142
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41 #: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters" msgid "Characters"
msgstr "Персонажи" msgstr "Персонажи"
#: allianceauth/authentication/templates/authentication/dashboard.html:129 #: allianceauth/authentication/templates/authentication/dashboard.html:150
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -97,13 +97,13 @@ msgstr "Персонажи"
msgid "Name" msgid "Name"
msgstr "Имя" msgstr "Имя"
#: allianceauth/authentication/templates/authentication/dashboard.html:130 #: allianceauth/authentication/templates/authentication/dashboard.html:151
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "Корпорация" msgstr "Корпорация"
#: allianceauth/authentication/templates/authentication/dashboard.html:131 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
#: allianceauth/hrapplications/templates/hrapplications/view.html:47 #: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance" msgid "Alliance"
@@ -391,7 +391,7 @@ msgstr "Пользователь"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -474,7 +474,6 @@ msgstr "Флот"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -483,8 +482,8 @@ msgstr "Создатель"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "Продолжительность" msgstr "Продолжительность"
@@ -569,11 +568,105 @@ msgstr "Флотовое участие зарегистрированно."
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "ФлАк ссылка устарела" msgstr "ФлАк ссылка устарела"
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "Управление Группой" msgstr "Управление Группой"
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -601,7 +694,7 @@ msgstr "Тип"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -667,6 +760,7 @@ msgstr "Группы"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "Описание" msgstr "Описание"
@@ -863,24 +957,24 @@ msgstr "Вы уже участник этой группы."
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "Вы уже подали заявку на вступление этой группы." msgstr "Вы уже подали заявку на вступление этой группы."
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "Вступить в группу %(group)s." msgstr "Вступить в группу %(group)s."
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "Вы не можете покинуть эту группу" msgstr "Вы не можете покинуть эту группу"
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "Вы не участник группыы" msgstr "Вы не участник группыы"
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "Ваш запрос находится на рассмотрении" msgstr "Ваш запрос находится на рассмотрении"
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "Запрос на выход из группы %(group)s." msgstr "Запрос на выход из группы %(group)s."
@@ -1141,43 +1235,56 @@ msgstr "Удалить все прочитанные уведомления"
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "Флотовые операции" msgstr "Флотовые операции"
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "Доктрина" msgstr "Доктрина"
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "Начало" msgstr "Начало"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "Название операции" msgstr "Название операции"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "ФлитКом" msgstr "ФлитКом"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Дополнительная информация"
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "Создать операцию" msgstr "Создать операцию"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "Система сбора" msgstr "Система сбора"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "Локальное время" msgstr "Локальное время"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "ФК" msgstr "ФК"
@@ -1195,9 +1302,8 @@ msgid "Current Eve Time:"
msgstr "Текущий EVE Time:" msgstr "Текущий EVE Time:"
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers" msgstr ""
msgstr "Следующие таймера"
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
#: allianceauth/timerboard/templates/timerboard/view.html:363 #: allianceauth/timerboard/templates/timerboard/view.html:363
@@ -1205,9 +1311,8 @@ msgid "No upcoming timers."
msgstr "Нет предстоящих таймеров" msgstr "Нет предстоящих таймеров"
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers" msgstr ""
msgstr "Прошлые таймера"
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:536 #: allianceauth/timerboard/templates/timerboard/view.html:536
@@ -1224,17 +1329,17 @@ msgstr "Обновить Флотовые операции"
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "Флотовая операция не существует" msgstr "Флотовая операция не существует"
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "Таймер для %(opname)s назначен." msgstr "Таймер для %(opname)s назначен."
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "Таймер для %(opname)s удалено. " msgstr "Таймер для %(opname)s удалено. "
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "Таймер для %(opname)sобновлен." msgstr "Таймер для %(opname)sобновлен."
@@ -1398,11 +1503,11 @@ msgstr "Пароль"
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "Пароль должен быть не менее 8 символов." msgstr "Пароль должен быть не менее 8 символов."
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "Discord персонаж отключен" msgstr "Discord персонаж отключен"
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1735,10 +1840,6 @@ msgstr "Флотовое время"
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "Флотовая Доктрина" msgstr "Флотовая Доктрина"
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "Дополнительная информация"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1950,12 +2051,12 @@ msgstr ""
"Ваш SRP запрос Killmail неправильный. Пожалуйста убедитесь в правильности " "Ваш SRP запрос Killmail неправильный. Пожалуйста убедитесь в правильности "
"ссылки. " "ссылки. "
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "Запрос SRP на Ваш %(ship)s утвержден." msgstr "Запрос SRP на Ваш %(ship)s утвержден."
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
@@ -1964,40 +2065,40 @@ msgstr ""
"Персонаж %(charid)s больше не имеет авторизации с Вашим аккаунтом. " "Персонаж %(charid)s больше не имеет авторизации с Вашим аккаунтом. "
"Пожалуйста перепроверьте ключ доступа." "Пожалуйста перепроверьте ключ доступа."
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "Нет SRP выбранных запросов" msgstr "Нет SRP выбранных запросов"
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "Не могу найти выбранный SRP запрос." msgstr "Не могу найти выбранный SRP запрос."
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "Удален %(numrequests)sиз SRP запросов." msgstr "Удален %(numrequests)sиз SRP запросов."
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "Утвержден %(numrequests)s SRP запрос." msgstr "Утвержден %(numrequests)s SRP запрос."
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "Невозможно найти выбранный SRP запрос" msgstr "Невозможно найти выбранный SRP запрос"
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "SRP запрос %(numrequests)s отказано." msgstr "SRP запрос %(numrequests)s отказано."
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "Невозможно найти SRP запрос с ID %(requestid)s." msgstr "Невозможно найти SRP запрос с ID %(requestid)s."
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "Сохранены изменения в SRP флот %(fleetname)s" msgstr "Сохранены изменения в SRP флот %(fleetname)s"
@@ -2226,6 +2327,14 @@ msgstr "Корпоративные таймера"
msgid "Structure" msgid "Structure"
msgstr "Структура" msgstr "Структура"
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr "Следующие таймера"
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr "Прошлые таймера"
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-30 14:36+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Aaron BuBu <351793078@qq.com>, 2020\n" "Last-Translator: Aaron BuBu <351793078@qq.com>, 2020\n"
"Language-Team: Chinese Simplified (https://www.transifex.com/alliance-auth/teams/107430/zh-Hans/)\n" "Language-Team: Chinese Simplified (https://www.transifex.com/alliance-auth/teams/107430/zh-Hans/)\n"
@@ -385,7 +385,7 @@ msgstr "用户"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:29
#: allianceauth/optimer/form.py:7 allianceauth/timerboard/form.py:59 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:59
#: allianceauth/timerboard/templates/timerboard/view.html:34 #: allianceauth/timerboard/templates/timerboard/view.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:201 #: allianceauth/timerboard/templates/timerboard/view.html:201
#: allianceauth/timerboard/templates/timerboard/view.html:374 #: allianceauth/timerboard/templates/timerboard/view.html:374
@@ -462,7 +462,6 @@ msgstr "舰队"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:49
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:74
#: allianceauth/optimer/templates/optimer/fleetoptable.html:17
#: allianceauth/timerboard/templates/timerboard/view.html:38 #: allianceauth/timerboard/templates/timerboard/view.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:205 #: allianceauth/timerboard/templates/timerboard/view.html:205
#: allianceauth/timerboard/templates/timerboard/view.html:378 #: allianceauth/timerboard/templates/timerboard/view.html:378
@@ -471,8 +470,8 @@ msgstr "创建者"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:51
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:77
#: allianceauth/optimer/form.py:9 #: allianceauth/optimer/form.py:18
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14 #: allianceauth/optimer/templates/optimer/fleetoptable.html:15
msgid "Duration" msgid "Duration"
msgstr "持续时间" msgstr "持续时间"
@@ -557,11 +556,105 @@ msgstr "成功注册舰队PAP"
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "PAP链接已过期" msgstr "PAP链接已过期"
#: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/admin.py:230
msgid "(auto)"
msgstr ""
#: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name."
msgstr ""
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
msgid "Group Management" msgid "Group Management"
msgstr "用户组管理" msgstr "用户组管理"
#: allianceauth/groupmanagement/models.py:102
msgid ""
"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>"
msgstr ""
#: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link."
msgstr ""
#: allianceauth/groupmanagement/models.py:116
msgid ""
"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."
msgstr ""
#: allianceauth/groupmanagement/models.py:123
msgid ""
"Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>Auth will not "
"remove users from this group automatically when they are no longer "
"authenticated."
msgstr ""
#: allianceauth/groupmanagement/models.py:134
msgid ""
"Group leaders can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:144
msgid ""
"Members of leader groups can process requests for this group. Use the "
"<code>auth.group_management</code> permission to allow a user to manage all "
"groups.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:153
msgid ""
"States listed here will have the ability to join this group provided they "
"have the proper permissions.<br>"
msgstr ""
#: allianceauth/groupmanagement/models.py:161
msgid ""
"Short description <i>(max. 512 characters)</i> of the group shown to users."
msgstr ""
#: allianceauth/groupmanagement/models.py:168
msgid "Can request non-public groups"
msgstr ""
#: allianceauth/groupmanagement/models.py:189
msgid "name"
msgstr ""
#: allianceauth/groupmanagement/models.py:192
msgid "Name that can not be used for groups."
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "reason"
msgstr ""
#: allianceauth/groupmanagement/models.py:195
msgid "Reason why this name is reserved."
msgstr ""
#: allianceauth/groupmanagement/models.py:198
msgid "created by"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "created at"
msgstr ""
#: allianceauth/groupmanagement/models.py:203
msgid "Date when this entry was created"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:5
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:14
msgid "Audit Log" msgid "Audit Log"
@@ -589,7 +682,7 @@ msgstr "类型"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33
#: allianceauth/notifications/templates/notifications/list.html:35 #: allianceauth/notifications/templates/notifications/list.html:35
#: allianceauth/notifications/templates/notifications/list.html:65 #: allianceauth/notifications/templates/notifications/list.html:65
#: allianceauth/optimer/templates/optimer/fleetoptable.html:18 #: allianceauth/optimer/templates/optimer/fleetoptable.html:19
#: allianceauth/services/templates/services/services.html:18 #: allianceauth/services/templates/services/services.html:18
#: allianceauth/timerboard/templates/timerboard/view.html:40 #: allianceauth/timerboard/templates/timerboard/view.html:40
#: allianceauth/timerboard/templates/timerboard/view.html:207 #: allianceauth/timerboard/templates/timerboard/view.html:207
@@ -655,6 +748,7 @@ msgstr "群组"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:25
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16 #: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10
msgid "Description" msgid "Description"
msgstr "描述" msgstr "描述"
@@ -847,24 +941,24 @@ msgstr "你已经是那个群组的一员了。"
msgid "You already have a pending application for that group." msgid "You already have a pending application for that group."
msgstr "你已经有了该组的未决申请" msgstr "你已经有了该组的未决申请"
#: allianceauth/groupmanagement/views.py:362 #: allianceauth/groupmanagement/views.py:363
#, python-format #, python-format
msgid "Applied to group %(group)s." msgid "Applied to group %(group)s."
msgstr "修改已经应用到%(group)s啦" msgstr "修改已经应用到%(group)s啦"
#: allianceauth/groupmanagement/views.py:372 #: allianceauth/groupmanagement/views.py:373
msgid "You cannot leave that group" msgid "You cannot leave that group"
msgstr "你无法离开那个用户组" msgstr "你无法离开那个用户组"
#: allianceauth/groupmanagement/views.py:376 #: allianceauth/groupmanagement/views.py:377
msgid "You are not a member of that group" msgid "You are not a member of that group"
msgstr "你不是那个用户组的成员" msgstr "你不是那个用户组的成员"
#: allianceauth/groupmanagement/views.py:388 #: allianceauth/groupmanagement/views.py:389
msgid "You already have a pending leave request for that group." msgid "You already have a pending leave request for that group."
msgstr "你已经有了该组的未决离开请求" msgstr "你已经有了该组的未决离开请求"
#: allianceauth/groupmanagement/views.py:403 #: allianceauth/groupmanagement/views.py:405
#, python-format #, python-format
msgid "Applied to leave group %(group)s." msgid "Applied to leave group %(group)s."
msgstr "已经离开群组%(group)s" msgstr "已经离开群组%(group)s"
@@ -1125,43 +1219,56 @@ msgstr "删除所有已读通知"
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "起队搞事" msgstr "起队搞事"
#: allianceauth/optimer/form.py:6 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:10 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
msgid "Doctrine" msgid "Doctrine"
msgstr "船型" msgstr "船型"
#: allianceauth/optimer/form.py:8 #: allianceauth/optimer/form.py:14
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12 #: allianceauth/optimer/templates/optimer/fleetoptable.html:13
msgid "Start Time" msgid "Start Time"
msgstr "集合时间" msgstr "集合时间"
#: allianceauth/optimer/form.py:10 #: allianceauth/optimer/form.py:15
#: allianceauth/optimer/templates/optimer/fleetoptable.html:9 #: allianceauth/optimer/templates/optimer/fleetoptable.html:9
msgid "Operation Name" msgid "Operation Name"
msgstr "搞事名目" msgstr "搞事名目"
#: allianceauth/optimer/form.py:11 #: allianceauth/optimer/form.py:16
msgid "Operation Type"
msgstr ""
#: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "FC" msgstr "FC"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "其他信息"
#: allianceauth/optimer/form.py:23
msgid "(Optional) Describe the operation with a couple of short words."
msgstr ""
#: allianceauth/optimer/templates/optimer/add.html:7 #: allianceauth/optimer/templates/optimer/add.html:7
#: allianceauth/optimer/templates/optimer/management.html:14 #: allianceauth/optimer/templates/optimer/management.html:14
msgid "Create Operation" msgid "Create Operation"
msgstr "起一个队" msgstr "起一个队"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:12
msgid "Form Up System" msgid "Form Up System"
msgstr "集结点" msgstr "集结点"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:13 #: allianceauth/optimer/templates/optimer/fleetoptable.html:14
#: allianceauth/timerboard/templates/timerboard/view.html:37 #: allianceauth/timerboard/templates/timerboard/view.html:37
#: allianceauth/timerboard/templates/timerboard/view.html:204 #: allianceauth/timerboard/templates/timerboard/view.html:204
#: allianceauth/timerboard/templates/timerboard/view.html:377 #: allianceauth/timerboard/templates/timerboard/view.html:377
msgid "Local Time" msgid "Local Time"
msgstr "本地时间" msgstr "本地时间"
#: allianceauth/optimer/templates/optimer/fleetoptable.html:15 #: allianceauth/optimer/templates/optimer/fleetoptable.html:16
msgid "FC" msgid "FC"
msgstr "FC" msgstr "FC"
@@ -1179,9 +1286,8 @@ msgid "Current Eve Time:"
msgstr "当前EVE游戏内时间" msgstr "当前EVE游戏内时间"
#: allianceauth/optimer/templates/optimer/management.html:27 #: allianceauth/optimer/templates/optimer/management.html:27
#: allianceauth/timerboard/templates/timerboard/view.html:194 msgid "Next Fleet Operations"
msgid "Next Timers" msgstr ""
msgstr "接下来的时间节点"
#: allianceauth/optimer/templates/optimer/management.html:31 #: allianceauth/optimer/templates/optimer/management.html:31
#: allianceauth/timerboard/templates/timerboard/view.html:363 #: allianceauth/timerboard/templates/timerboard/view.html:363
@@ -1189,9 +1295,8 @@ msgid "No upcoming timers."
msgstr "没有快到的时间节点,歇一会吧" msgstr "没有快到的时间节点,歇一会吧"
#: allianceauth/optimer/templates/optimer/management.html:34 #: allianceauth/optimer/templates/optimer/management.html:34
#: allianceauth/timerboard/templates/timerboard/view.html:367 msgid "Past Fleet Operations"
msgid "Past Timers" msgstr ""
msgstr "已经过去的时间节点"
#: allianceauth/optimer/templates/optimer/management.html:38 #: allianceauth/optimer/templates/optimer/management.html:38
#: allianceauth/timerboard/templates/timerboard/view.html:536 #: allianceauth/timerboard/templates/timerboard/view.html:536
@@ -1208,17 +1313,17 @@ msgstr "更新搞事队"
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "这搞事队不存在啊,你会不会搞事啊" msgstr "这搞事队不存在啊,你会不会搞事啊"
#: allianceauth/optimer/views.py:55 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
msgid "Created operation timer for %(opname)s." msgid "Created operation timer for %(opname)s."
msgstr "为%(opname)s创建了搞事时间节点冲鸭" msgstr "为%(opname)s创建了搞事时间节点冲鸭"
#: allianceauth/optimer/views.py:73 #: allianceauth/optimer/views.py:87
#, python-format #, python-format
msgid "Removed operation timer for %(opname)s." msgid "Removed operation timer for %(opname)s."
msgstr "移除了%(opname)s的搞事时间节点咕咕咕" msgstr "移除了%(opname)s的搞事时间节点咕咕咕"
#: allianceauth/optimer/views.py:96 #: allianceauth/optimer/views.py:125
#, python-format #, python-format
msgid "Saved changes to operation timer for %(opname)s." msgid "Saved changes to operation timer for %(opname)s."
msgstr "对搞事时间节点%(opname)s的修改保存了朝令夕改你是不是合格FC啊" msgstr "对搞事时间节点%(opname)s的修改保存了朝令夕改你是不是合格FC啊"
@@ -1382,11 +1487,11 @@ msgstr "密码"
msgid "Password must be at least 8 characters long." msgid "Password must be at least 8 characters long."
msgstr "密码至少要有8个字符啊你也太不注重安全啦" msgstr "密码至少要有8个字符啊你也太不注重安全啦"
#: allianceauth/services/modules/discord/models.py:225 #: allianceauth/services/modules/discord/models.py:234
msgid "Discord Account Disabled" msgid "Discord Account Disabled"
msgstr "" msgstr ""
#: allianceauth/services/modules/discord/models.py:227 #: allianceauth/services/modules/discord/models.py:236
msgid "" msgid ""
"Your Discord account was disabled automatically by Auth. If you think this " "Your Discord account was disabled automatically by Auth. If you think this "
"was a mistake, please contact an admin." "was a mistake, please contact an admin."
@@ -1711,10 +1816,6 @@ msgstr "集结时间"
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "舰队船型" msgstr "舰队船型"
#: allianceauth/srp/form.py:14 allianceauth/srp/templates/srp/data.html:93
msgid "Additional Info"
msgstr "其他信息"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
msgstr "" msgstr ""
@@ -1924,52 +2025,52 @@ msgid ""
"zKillboard." "zKillboard."
msgstr "小老弟你这个补损用的KB链接不对劲儿啊你是不是没用zKillboard啊" msgstr "小老弟你这个补损用的KB链接不对劲儿啊你是不是没用zKillboard啊"
#: allianceauth/srp/views.py:211 #: allianceauth/srp/views.py:212
#, python-format #, python-format
msgid "Submitted SRP request for your %(ship)s." msgid "Submitted SRP request for your %(ship)s."
msgstr "你的%(ship)s的补损申请好啦" msgstr "你的%(ship)s的补损申请好啦"
#: allianceauth/srp/views.py:215 #: allianceauth/srp/views.py:216
#, python-format #, python-format
msgid "" msgid ""
"Character %(charid)s does not belong to your Auth account. Please add the " "Character %(charid)s does not belong to your Auth account. Please add the "
"API key for this character and try again" "API key for this character and try again"
msgstr "%(charid)s这个角色好像不在你的账号里啊你交没交ESI啊交过再试一次吧" msgstr "%(charid)s这个角色好像不在你的账号里啊你交没交ESI啊交过再试一次吧"
#: allianceauth/srp/views.py:235 allianceauth/srp/views.py:261 #: allianceauth/srp/views.py:236 allianceauth/srp/views.py:262
#: allianceauth/srp/views.py:299 #: allianceauth/srp/views.py:300
msgid "No SRP requests selected" msgid "No SRP requests selected"
msgstr "你没选中任何补损请求哦" msgstr "你没选中任何补损请求哦"
#: allianceauth/srp/views.py:246 allianceauth/srp/views.py:284 #: allianceauth/srp/views.py:247 allianceauth/srp/views.py:285
msgid "Unable to locate selected SRP request." msgid "Unable to locate selected SRP request."
msgstr "你选的这条补损请求找不到呀" msgstr "你选的这条补损请求找不到呀"
#: allianceauth/srp/views.py:249 #: allianceauth/srp/views.py:250
#, python-format #, python-format
msgid "Deleted %(numrequests)s SRP requests" msgid "Deleted %(numrequests)s SRP requests"
msgstr "删了%(numrequests)s条补损请求你是不是准备赖账啊" msgstr "删了%(numrequests)s条补损请求你是不是准备赖账啊"
#: allianceauth/srp/views.py:287 #: allianceauth/srp/views.py:288
#, python-format #, python-format
msgid "Approved %(numrequests)s SRP requests" msgid "Approved %(numrequests)s SRP requests"
msgstr "通过了%(numrequests)s条补损请求钱包大出血" msgstr "通过了%(numrequests)s条补损请求钱包大出血"
#: allianceauth/srp/views.py:319 #: allianceauth/srp/views.py:320
msgid "Unable to locate selected SRP request" msgid "Unable to locate selected SRP request"
msgstr "你选的这条补损请求找不到呀" msgstr "你选的这条补损请求找不到呀"
#: allianceauth/srp/views.py:322 #: allianceauth/srp/views.py:323
#, python-format #, python-format
msgid "Rejected %(numrequests)s SRP requests." msgid "Rejected %(numrequests)s SRP requests."
msgstr "已拒绝%(numrequests)s个补损申请小老弟你这是想赖账" msgstr "已拒绝%(numrequests)s个补损申请小老弟你这是想赖账"
#: allianceauth/srp/views.py:335 #: allianceauth/srp/views.py:336
#, python-format #, python-format
msgid "Unable to locate SRP request with ID %(requestid)s" msgid "Unable to locate SRP request with ID %(requestid)s"
msgstr "找不到ID是%(requestid)s的补损申请呀老哥眼花了" msgstr "找不到ID是%(requestid)s的补损申请呀老哥眼花了"
#: allianceauth/srp/views.py:359 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "你做的修改已经保存到%(fleetname)s这个补损舰队啦尽情白给吧" msgstr "你做的修改已经保存到%(fleetname)s这个补损舰队啦尽情白给吧"
@@ -2195,6 +2296,14 @@ msgstr "公司时间表"
msgid "Structure" msgid "Structure"
msgstr "建筑" msgstr "建筑"
#: allianceauth/timerboard/templates/timerboard/view.html:194
msgid "Next Timers"
msgstr "接下来的时间节点"
#: allianceauth/timerboard/templates/timerboard/view.html:367
msgid "Past Timers"
msgstr "已经过去的时间节点"
#: allianceauth/timerboard/views.py:74 #: allianceauth/timerboard/views.py:74
#, python-format #, python-format
msgid "Added new timer in %(system)s at %(time)s." msgid "Added new timer in %(system)s at %(time)s."

View File

@@ -49,19 +49,22 @@ class NotificationManager(models.Manager):
logger.info("Created notification %s", obj) logger.info("Created notification %s", obj)
return obj return obj
def _max_notifications_per_user(self): def _max_notifications_per_user(self) -> int:
"""return the maximum number of notifications allowed per user""" """Maximum number of notifications allowed per user."""
max_notifications = getattr(settings, 'NOTIFICATIONS_MAX_PER_USER', None) max_notifications = getattr(
if ( settings,
max_notifications is None "NOTIFICATIONS_MAX_PER_USER",
or not isinstance(max_notifications, int) self.model.NOTIFICATIONS_MAX_PER_USER_DEFAULT
or max_notifications < 0 )
): try:
max_notifications = int(max_notifications)
except ValueError:
max_notifications = None
if max_notifications is None or max_notifications < 0:
logger.warning( logger.warning(
'NOTIFICATIONS_MAX_PER_USER setting is invalid. Using default.' "NOTIFICATIONS_MAX_PER_USER setting is invalid. Using default."
) )
max_notifications = self.model.NOTIFICATIONS_MAX_PER_USER_DEFAULT max_notifications = self.model.NOTIFICATIONS_MAX_PER_USER_DEFAULT
return max_notifications return max_notifications
def user_unread_count(self, user_pk: int) -> int: def user_unread_count(self, user_pk: int) -> int:

View File

@@ -1,5 +1,6 @@
from unittest.mock import patch from unittest.mock import patch
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
@@ -113,29 +114,53 @@ class TestUserNotify(TestCase):
self.assertSetEqual(result, expected) self.assertSetEqual(result, expected)
@patch("allianceauth.notifications.managers.logger")
@patch( @patch(
MODULE_PATH + '.Notification.NOTIFICATIONS_MAX_PER_USER_DEFAULT', MODULE_PATH + ".Notification.NOTIFICATIONS_MAX_PER_USER_DEFAULT",
NOTIFICATIONS_MAX_PER_USER_DEFAULT NOTIFICATIONS_MAX_PER_USER_DEFAULT
) )
class TestMaxNotificationsPerUser(TestCase): class TestMaxNotificationsPerUser(TestCase):
@override_settings(NOTIFICATIONS_MAX_PER_USER=42)
@override_settings(NOTIFICATIONS_MAX_PER_USER=None) def test_should_use_custom_integer_setting(self, mock_logger):
def test_reset_to_default_if_not_defined(self): # when
result = Notification.objects._max_notifications_per_user() result = Notification.objects._max_notifications_per_user()
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT # then
self.assertEqual(result, expected) self.assertEqual(result, 42)
self.assertFalse(mock_logger.warning.called)
@override_settings(NOTIFICATIONS_MAX_PER_USER='11') @override_settings(NOTIFICATIONS_MAX_PER_USER="42")
def test_reset_to_default_if_not_int(self): def test_should_use_custom_string_setting(self, mock_logger):
# when
result = Notification.objects._max_notifications_per_user() result = Notification.objects._max_notifications_per_user()
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT # then
self.assertEqual(result, expected) self.assertEqual(result, 42)
self.assertFalse(mock_logger.warning.called)
@override_settings()
def test_should_use_default_if_not_defined(self, mock_logger):
# given
del settings.NOTIFICATIONS_MAX_PER_USER
# when
result = Notification.objects._max_notifications_per_user()
# then
self.assertEqual(result, NOTIFICATIONS_MAX_PER_USER_DEFAULT)
self.assertFalse(mock_logger.warning.called)
@override_settings(NOTIFICATIONS_MAX_PER_USER="abc")
def test_should_reset_to_default_if_not_int(self, mock_logger):
# when
result = Notification.objects._max_notifications_per_user()
# then
self.assertEqual(result, NOTIFICATIONS_MAX_PER_USER_DEFAULT)
self.assertTrue(mock_logger.warning.called)
@override_settings(NOTIFICATIONS_MAX_PER_USER=-1) @override_settings(NOTIFICATIONS_MAX_PER_USER=-1)
def test_reset_to_default_if_lt_zero(self): def test_should_reset_to_default_if_lt_zero(self, mock_logger):
# when
result = Notification.objects._max_notifications_per_user() result = Notification.objects._max_notifications_per_user()
expected = NOTIFICATIONS_MAX_PER_USER_DEFAULT # then
self.assertEqual(result, expected) self.assertEqual(result, NOTIFICATIONS_MAX_PER_USER_DEFAULT)
self.assertTrue(mock_logger.warning.called)
@patch('allianceauth.notifications.managers.cache') @patch('allianceauth.notifications.managers.cache')

View File

@@ -73,6 +73,8 @@
], ],
bootstrap: true bootstrap: true
}, },
"stateSave": true,
"stateDuration": 0,
drawCallback: function ( settings ) { drawCallback: function ( settings ) {
let api = this.api(); let api = this.api();
let rows = api.rows( {page:'current'} ).nodes(); let rows = api.rows( {page:'current'} ).nodes();

View File

@@ -106,8 +106,10 @@
idx: 1 idx: 1
} }
], ],
bootstrap: true bootstrap: true,
}, },
"stateSave": true,
"stateDuration": 0,
drawCallback: function ( settings ) { drawCallback: function ( settings ) {
let api = this.api(); let api = this.api();
let rows = api.rows( {page:'current'} ).nodes(); let rows = api.rows( {page:'current'} ).nodes();

View File

@@ -167,7 +167,7 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Bootstrap messaging css workaround # Bootstrap messaging css workaround
MESSAGE_TAGS = { MESSAGE_TAGS = {
messages.ERROR: 'danger' messages.ERROR: 'danger error'
} }
CACHES = { CACHES = {

View File

@@ -1,4 +1,5 @@
from copy import copy from copy import copy
from typing import Set, Iterable
class DiscordRoles: class DiscordRoles:
@@ -39,7 +40,7 @@ class DiscordRoles:
def __len__(self): def __len__(self):
return len(self._roles.keys()) return len(self._roles.keys())
def has_roles(self, role_ids: set) -> bool: def has_roles(self, role_ids: Set[int]) -> bool:
"""returns true if this objects contains all roles defined by given role_ids """returns true if this objects contains all roles defined by given role_ids
incl. managed roles incl. managed roles
""" """
@@ -47,13 +48,22 @@ class DiscordRoles:
all_role_ids = self._roles.keys() all_role_ids = self._roles.keys()
return role_ids.issubset(all_role_ids) return role_ids.issubset(all_role_ids)
def ids(self) -> set: def ids(self) -> Set[int]:
"""return a set of all role IDs""" """return a set of all role IDs"""
return set(self._roles.keys()) return set(self._roles.keys())
def subset(self, role_ids: set = None, managed_only: bool = False) -> object: def subset(
"""returns a new object containing the subset of roles as defined self,
by given role IDs and/or including managed roles only role_ids: Iterable[int] = None,
managed_only: bool = False,
role_names: Iterable[str] = None
) -> "DiscordRoles":
"""returns a new object containing the subset of roles
Args:
- role_ids: role ids must be in the provided list
- managed_only: roles must be managed
- role_names: role names must match provided list (not case sensitive)
""" """
if role_ids is not None: if role_ids is not None:
role_ids = {int(id) for id in role_ids} role_ids = {int(id) for id in role_ids}
@@ -74,15 +84,21 @@ class DiscordRoles:
if role_id in role_ids and role['managed'] if role_id in role_ids and role['managed']
]) ])
else: elif role_ids is None and managed_only is False and role_names is not None:
return copy(self) role_names = {self.sanitize_role_name(name).lower() for name in role_names}
return type(self)([
role for role in self._roles.values()
if role["name"].lower() in role_names
])
def union(self, other: object) -> object: return copy(self)
def union(self, other: object) -> "DiscordRoles":
"""returns a new roles object that is the union of this roles object """returns a new roles object that is the union of this roles object
with other""" with other"""
return type(self)(list(self) + list(other)) return type(self)(list(self) + list(other))
def difference(self, other: object) -> object: def difference(self, other: object) -> "DiscordRoles":
"""returns a new roles object that only contains the roles """returns a new roles object that only contains the roles
that exist in the current objects, but not in other that exist in the current objects, but not in other
""" """
@@ -94,11 +110,10 @@ class DiscordRoles:
role_name = self.sanitize_role_name(role_name) role_name = self.sanitize_role_name(role_name)
if role_name in self._roles_by_name: if role_name in self._roles_by_name:
return self._roles_by_name[role_name] return self._roles_by_name[role_name]
else: return dict()
return dict()
@classmethod @classmethod
def create_from_matched_roles(cls, matched_roles: list) -> None: def create_from_matched_roles(cls, matched_roles: list) -> "DiscordRoles":
"""returns a new object created from the given list of matches roles """returns a new object created from the given list of matches roles
matches_roles must be a list of tuples in the form: (role, created) matches_roles must be a list of tuples in the form: (role, created)
@@ -107,7 +122,7 @@ class DiscordRoles:
return cls(raw_roles) return cls(raw_roles)
@staticmethod @staticmethod
def _assert_valid_role(role: dict): def _assert_valid_role(role: dict) -> None:
if not isinstance(role, dict): if not isinstance(role, dict):
raise TypeError('Roles must be of type dict: %s' % role) raise TypeError('Roles must be of type dict: %s' % role)

View File

@@ -6,7 +6,7 @@ TEST_BOT_TOKEN = 'abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY'
TEST_ROLE_ID = 654321012345678912 TEST_ROLE_ID = 654321012345678912
def create_role(id: int, name: str, managed=False): def create_role(id: int, name: str, managed=False) -> dict:
return { return {
'id': int(id), 'id': int(id),
'name': str(name), 'name': str(name),
@@ -21,8 +21,10 @@ def create_matched_role(role, created=False) -> tuple:
ROLE_ALPHA = create_role(1, 'alpha') ROLE_ALPHA = create_role(1, 'alpha')
ROLE_BRAVO = create_role(2, 'bravo') ROLE_BRAVO = create_role(2, 'bravo')
ROLE_CHARLIE = create_role(3, 'charlie') ROLE_CHARLIE = create_role(3, 'charlie')
ROLE_CHARLIE_2 = create_role(4, 'Charlie') # Discord roles are case sensitive
ROLE_MIKE = create_role(13, 'mike', True) ROLE_MIKE = create_role(13, 'mike', True)
ALL_ROLES = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE] ALL_ROLES = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]

View File

@@ -1,6 +1,14 @@
from unittest import TestCase from unittest import TestCase
from . import ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ALL_ROLES, create_role from . import (
ROLE_ALPHA,
ROLE_BRAVO,
ROLE_CHARLIE,
ROLE_CHARLIE_2,
ROLE_MIKE,
ALL_ROLES,
create_role
)
from .. import DiscordRoles from .. import DiscordRoles
@@ -143,6 +151,16 @@ class TestSubset(TestCase):
expected = {1, 2, 3, 13} expected = {1, 2, 3, 13}
self.assertSetEqual(roles.ids(), expected) self.assertSetEqual(roles.ids(), expected)
def test_should_return_role_names_only(self):
# given
all_roles = DiscordRoles([
ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2
])
# when
roles = all_roles.subset(role_names={"bravo", "charlie"})
# then
self.assertSetEqual(roles.ids(), {2, 3, 4})
class TestHasRoles(TestCase): class TestHasRoles(TestCase):

View File

@@ -6,11 +6,12 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
from allianceauth.groupmanagement.models import ReservedGroupName
from allianceauth.notifications import notify from allianceauth.notifications import notify
from . import __title__ from . import __title__
from .app_settings import DISCORD_GUILD_ID from .app_settings import DISCORD_GUILD_ID
from .discord_client import DiscordApiBackoff, DiscordRoles from .discord_client import DiscordApiBackoff, DiscordClient, DiscordRoles
from .discord_client.helpers import match_or_create_roles_from_names from .discord_client.helpers import match_or_create_roles_from_names
from .managers import DiscordUserManager from .managers import DiscordUserManager
from .utils import LoggerAddTag from .utils import LoggerAddTag
@@ -109,11 +110,16 @@ class DiscordUser(models.Model):
- False on error or raises exception - False on error or raises exception
""" """
client = DiscordUser.objects._bot_client() client = DiscordUser.objects._bot_client()
member_roles = self._determine_member_roles(client)
if member_roles is None:
return None
return self._update_roles_if_needed(client, state_name, member_roles)
def _determine_member_roles(self, client: DiscordClient) -> DiscordRoles:
"""Determine the roles of the current member / user."""
member_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid) member_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
if member_info is None: if member_info is None:
# User is no longer a member return None # User is no longer a member
return None
guild_roles = DiscordRoles(client.guild_roles(guild_id=DISCORD_GUILD_ID)) guild_roles = DiscordRoles(client.guild_roles(guild_id=DISCORD_GUILD_ID))
logger.debug('Current guild roles: %s', guild_roles.ids()) logger.debug('Current guild roles: %s', guild_roles.ids())
if 'roles' in member_info: if 'roles' in member_info:
@@ -128,10 +134,13 @@ class DiscordUser(models.Model):
set(member_info['roles']).difference(guild_roles.ids()) set(member_info['roles']).difference(guild_roles.ids())
) )
) )
member_roles = guild_roles.subset(member_info['roles']) return guild_roles.subset(member_info['roles'])
else: raise RuntimeError('member_info from %s is not valid' % self.user)
raise RuntimeError('member_info from %s is not valid' % self.user)
def _update_roles_if_needed(
self, client: DiscordClient, state_name: str, member_roles: DiscordRoles
) -> bool:
"""Update the roles of this member/user if needed."""
requested_roles = match_or_create_roles_from_names( requested_roles = match_or_create_roles_from_names(
client=client, client=client,
guild_id=DISCORD_GUILD_ID, guild_id=DISCORD_GUILD_ID,
@@ -143,10 +152,13 @@ class DiscordUser(models.Model):
'Requested roles for user %s: %s', self.user, requested_roles.ids() 'Requested roles for user %s: %s', self.user, requested_roles.ids()
) )
logger.debug('Current roles user %s: %s', self.user, member_roles.ids()) logger.debug('Current roles user %s: %s', self.user, member_roles.ids())
reserved_role_names = ReservedGroupName.objects.values_list("name", flat=True)
member_roles_reserved = member_roles.subset(role_names=reserved_role_names)
member_roles_managed = member_roles.subset(managed_only=True) member_roles_managed = member_roles.subset(managed_only=True)
if requested_roles != member_roles.difference(member_roles_managed): member_roles_persistent = member_roles_managed.union(member_roles_reserved)
if requested_roles != member_roles.difference(member_roles_persistent):
logger.debug('Need to update roles for user %s', self.user) logger.debug('Need to update roles for user %s', self.user)
new_roles = requested_roles.union(member_roles_managed) new_roles = requested_roles.union(member_roles_persistent)
success = client.modify_guild_member( success = client.modify_guild_member(
guild_id=DISCORD_GUILD_ID, guild_id=DISCORD_GUILD_ID,
user_id=self.uid, user_id=self.uid,
@@ -157,10 +169,8 @@ class DiscordUser(models.Model):
else: else:
logger.warning('Failed to update roles for %s', self.user) logger.warning('Failed to update roles for %s', self.user)
return success return success
logger.info('No need to update roles for user %s', self.user)
else: return True
logger.info('No need to update roles for user %s', self.user)
return True
def update_username(self) -> bool: def update_username(self) -> bool:
"""Updates the username incl. the discriminator """Updates the username incl. the discriminator
@@ -171,7 +181,6 @@ class DiscordUser(models.Model):
- None if user is no longer a member of the Discord server - None if user is no longer a member of the Discord server
- False on error or raises exception - False on error or raises exception
""" """
client = DiscordUser.objects._bot_client() client = DiscordUser.objects._bot_client()
user_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid) user_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
if user_info is None: if user_info is None:

View File

@@ -9,6 +9,7 @@ from ..discord_client.tests import ( # noqa
ROLE_ALPHA, ROLE_ALPHA,
ROLE_BRAVO, ROLE_BRAVO,
ROLE_CHARLIE, ROLE_CHARLIE,
ROLE_CHARLIE_2,
ROLE_MIKE, ROLE_MIKE,
ALL_ROLES, ALL_ROLES,
create_user_info create_user_info

View File

@@ -5,6 +5,7 @@ from requests.exceptions import HTTPError
from django.test import TestCase from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.groupmanagement.models import ReservedGroupName
from . import ( from . import (
TEST_USER_NAME, TEST_USER_NAME,
@@ -15,7 +16,8 @@ from . import (
ROLE_ALPHA, ROLE_ALPHA,
ROLE_BRAVO, ROLE_BRAVO,
ROLE_CHARLIE, ROLE_CHARLIE,
ROLE_MIKE ROLE_CHARLIE_2,
ROLE_MIKE,
) )
from ..discord_client import DiscordClient, DiscordApiBackoff from ..discord_client import DiscordClient, DiscordApiBackoff
from ..discord_client.tests import create_matched_role from ..discord_client.tests import create_matched_role
@@ -294,25 +296,33 @@ class TestUpdateGroups(TestCase):
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(set(kwargs['role_ids']), {1, 2}) self.assertEqual(set(kwargs['role_ids']), {1, 2})
def test_update_if_needed_and_preserve_managed_roles( def test_should_update_and_preserve_managed_and_reserved_roles(
self, self,
mock_user_group_names, mock_user_group_names,
mock_DiscordClient mock_DiscordClient
): ):
roles_current = [1, 13] # given
roles_current = [1, 3, 4, 13]
mock_user_group_names.return_value = [] mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\ mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested .return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles mock_DiscordClient.return_value.guild_roles.return_value = [
mock_DiscordClient.return_value.guild_member.return_value = \ ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2
{'roles': roles_current} ]
mock_DiscordClient.return_value.guild_member.return_value = {
'roles': roles_current
}
mock_DiscordClient.return_value.modify_guild_member.return_value = True mock_DiscordClient.return_value.modify_guild_member.return_value = True
ReservedGroupName.objects.create(
name="charlie", reason="dummy", created_by="xyz"
)
# when
result = self.discord_user.update_groups() result = self.discord_user.update_groups()
# then
self.assertTrue(result) self.assertTrue(result)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called) self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(set(kwargs['role_ids']), {1, 2, 13}) self.assertEqual(set(kwargs['role_ids']), {1, 2, 3, 4, 13})
def test_dont_update_if_not_needed( def test_dont_update_if_not_needed(
self, self,

View File

@@ -1,7 +1,8 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group
from .models import AuthTS, Teamspeak3User, StateGroup from .models import AuthTS, Teamspeak3User, StateGroup, TSgroup
from ...admin import ServicesUserAdmin from ...admin import ServicesUserAdmin
from allianceauth.groupmanagement.models import ReservedGroupName
@admin.register(Teamspeak3User) @admin.register(Teamspeak3User)
@@ -25,6 +26,16 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
fields = ('auth_group', 'ts_group') fields = ('auth_group', 'ts_group')
filter_horizontal = ('ts_group',) filter_horizontal = ('ts_group',)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'auth_group':
kwargs['queryset'] = Group.objects.exclude(name__in=ReservedGroupName.objects.values_list('name', flat=True))
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == 'ts_group':
kwargs['queryset'] = TSgroup.objects.exclude(ts_group_name__in=ReservedGroupName.objects.values_list('name', flat=True))
return super().formfield_for_manytomany(db_field, request, **kwargs)
def _ts_group(self, obj): def _ts_group(self, obj):
return [x for x in obj.ts_group.all().order_by('ts_group_id')] return [x for x in obj.ts_group.all().order_by('ts_group_id')]

View File

@@ -4,6 +4,7 @@ from django.conf import settings
from .util.ts3 import TS3Server, TeamspeakError from .util.ts3 import TS3Server, TeamspeakError
from .models import TSgroup from .models import TSgroup
from allianceauth.groupmanagement.models import ReservedGroupName
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -156,32 +157,25 @@ class Teamspeak3Manager:
logger.info(f"Removed user id {uid} from group id {groupid} on TS3 server.") logger.info(f"Removed user id {uid} from group id {groupid} on TS3 server.")
def _sync_ts_group_db(self): def _sync_ts_group_db(self):
logger.debug("_sync_ts_group_db function called.")
try: try:
remote_groups = self._group_list() remote_groups = self._group_list()
local_groups = TSgroup.objects.all() managed_groups = {g:int(remote_groups[g]) for g in remote_groups if g in set(remote_groups.keys()) - set(ReservedGroupName.objects.values_list('name', flat=True))}
logger.debug("Comparing remote groups to TSgroup objects: %s" % local_groups) remove = TSgroup.objects.exclude(ts_group_id__in=managed_groups.values())
for key in remote_groups:
logger.debug(f"Typecasting remote_group value at position {key} to int: {remote_groups[key]}") if remove:
remote_groups[key] = int(remote_groups[key]) logger.debug(f"Deleting {remove.count()} TSgroup models: not found on server, or reserved name.")
remove.delete()
add = {g:managed_groups[g] for g in managed_groups if managed_groups[g] in set(managed_groups.values()) - set(TSgroup.objects.values_list("ts_group_id", flat=True))}
if add:
logger.debug(f"Adding {len(add)} new TSgroup models.")
models = [TSgroup(ts_group_name=name, ts_group_id=add[name]) for name in add]
TSgroup.objects.bulk_create(models)
for group in local_groups:
logger.debug("Checking local group %s" % group)
if group.ts_group_id not in remote_groups.values():
logger.debug(
f"Local group id {group.ts_group_id} not found on server. Deleting model {group}")
TSgroup.objects.filter(ts_group_id=group.ts_group_id).delete()
for key in remote_groups:
g = TSgroup(ts_group_id=remote_groups[key], ts_group_name=key)
q = TSgroup.objects.filter(ts_group_id=g.ts_group_id)
if not q:
logger.debug("Local group does not exist for TS group {}. Creating TSgroup model {}".format(
remote_groups[key], g))
g.save()
except TeamspeakError as e: except TeamspeakError as e:
logger.error("Error occured while syncing TS group db: %s" % str(e)) logger.error(f"Error occurred while syncing TS group db: {str(e)}")
except: except Exception:
logger.exception("An unhandled exception has occured while syncing TS groups.") logger.exception(f"An unhandled exception has occurred while syncing TS groups.")
def add_user(self, user, fmt_name): def add_user(self, user, fmt_name):
username_clean = self.__santatize_username(fmt_name[:30]) username_clean = self.__santatize_username(fmt_name[:30])
@@ -240,7 +234,7 @@ class Teamspeak3Manager:
logger.exception(f"Failed to delete user id {uid} from TS3 - received response {ret}") logger.exception(f"Failed to delete user id {uid} from TS3 - received response {ret}")
return False return False
else: else:
logger.warn("User with id %s not found on TS3 server. Assuming succesful deletion." % uid) logger.warning("User with id %s not found on TS3 server. Assuming succesful deletion." % uid)
return True return True
def check_user_exists(self, uid): def check_user_exists(self, uid):
@@ -270,7 +264,8 @@ class Teamspeak3Manager:
addgroups.append(ts_groups[ts_group_key]) addgroups.append(ts_groups[ts_group_key])
for user_ts_group_key in user_ts_groups: for user_ts_group_key in user_ts_groups:
if user_ts_groups[user_ts_group_key] not in ts_groups.values(): if user_ts_groups[user_ts_group_key] not in ts_groups.values():
remgroups.append(user_ts_groups[user_ts_group_key]) if not ReservedGroupName.objects.filter(name=user_ts_group_key).exists():
remgroups.append(user_ts_groups[user_ts_group_key])
for g in addgroups: for g in addgroups:
logger.info(f"Adding Teamspeak user {userid} into group {g}") logger.info(f"Adding Teamspeak user {userid} into group {g}")

View File

@@ -5,16 +5,18 @@ from django import urls
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import signals from django.db.models import signals
from django.contrib.admin import AdminSite
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from .auth_hooks import Teamspeak3Service from .auth_hooks import Teamspeak3Service
from .models import Teamspeak3User, AuthTS, TSgroup, StateGroup from .models import Teamspeak3User, AuthTS, TSgroup, StateGroup
from .tasks import Teamspeak3Tasks from .tasks import Teamspeak3Tasks
from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts
from .admin import AuthTSgroupAdmin
from .manager import Teamspeak3Manager from .manager import Teamspeak3Manager
from .util.ts3 import TeamspeakError from .util.ts3 import TeamspeakError
from allianceauth.authentication.models import State from allianceauth.groupmanagement.models import ReservedGroupName
MODULE_PATH = 'allianceauth.services.modules.teamspeak3' MODULE_PATH = 'allianceauth.services.modules.teamspeak3'
DEFAULT_AUTH_GROUP = 'Member' DEFAULT_AUTH_GROUP = 'Member'
@@ -315,6 +317,9 @@ class Teamspeak3SignalsTestCase(TestCase):
class Teamspeak3ManagerTestCase(TestCase): class Teamspeak3ManagerTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.reserved = ReservedGroupName.objects.create(name='reserved', reason='tests', created_by='Bob, praise be!')
@staticmethod @staticmethod
def my_side_effect(*args, **kwargs): def my_side_effect(*args, **kwargs):
@@ -334,8 +339,135 @@ class Teamspeak3ManagerTestCase(TestCase):
manager._server = server manager._server = server
# create test data # create test data
user = User.objects.create_user("dummy") user = AuthUtils.create_user("dummy")
user.profile.state = State.objects.filter(name="Member").first() AuthUtils.assign_state(user, AuthUtils.get_member_state())
# perform test # perform test
manager.add_user(user, "Dummy User") manager.add_user(user, "Dummy User")
@mock.patch.object(Teamspeak3Manager, '_get_userid')
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
def test_update_groups_add(self, remove, add, groups, userid):
"""Add to one group"""
userid.return_value = 1
groups.return_value = {'test': 1}
Teamspeak3Manager().update_groups(1, {'test': 1, 'dummy': 2})
self.assertEqual(add.call_count, 1)
self.assertEqual(remove.call_count, 0)
self.assertEqual(add.call_args[0][1], 2)
@mock.patch.object(Teamspeak3Manager, '_get_userid')
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
def test_update_groups_remove(self, remove, add, groups, userid):
"""Remove from one group"""
userid.return_value = 1
groups.return_value = {'test': '1', 'dummy': '2'}
Teamspeak3Manager().update_groups(1, {'test': 1})
self.assertEqual(add.call_count, 0)
self.assertEqual(remove.call_count, 1)
self.assertEqual(remove.call_args[0][1], 2)
@mock.patch.object(Teamspeak3Manager, '_get_userid')
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
def test_update_groups_remove_reserved(self, remove, add, groups, userid):
"""Remove from one group, but do not touch reserved group"""
userid.return_value = 1
groups.return_value = {'test': 1, 'dummy': 2, self.reserved.name: 3}
Teamspeak3Manager().update_groups(1, {'test': 1})
self.assertEqual(add.call_count, 0)
self.assertEqual(remove.call_count, 1)
self.assertEqual(remove.call_args[0][1], 2)
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_create(self, group_list):
"""Populate the list of all TSgroups"""
group_list.return_value = {'allowed':'1', 'also allowed':'2'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 2)
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_delete(self, group_list):
"""Populate the list of all TSgroups, and delete one which no longer exists"""
TSgroup.objects.create(ts_group_name='deleted', ts_group_id=3)
group_list.return_value = {'allowed': '1'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
self.assertFalse(TSgroup.objects.filter(ts_group_name='deleted').exists())
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_dont_create_reserved(self, group_list):
"""Populate the list of all TSgroups, ignoring a reserved group name"""
group_list.return_value = {'allowed': '1', 'reserved': '4'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
self.assertFalse(TSgroup.objects.filter(ts_group_name='reserved').exists())
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_delete_reserved(self, group_list):
"""Populate the list of all TSgroups, deleting the TSgroup model for one which has become reserved"""
TSgroup.objects.create(ts_group_name='reserved', ts_group_id=4)
group_list.return_value = {'allowed': '1', 'reserved': '4'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
self.assertFalse(TSgroup.objects.filter(ts_group_name='reserved').exists())
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_partial_addition(self, group_list):
"""Some TSgroups already exist in database, add new ones"""
TSgroup.objects.create(ts_group_name='allowed', ts_group_id=1)
group_list.return_value = {'allowed': '1', 'also allowed': '2'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 2)
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_partial_removal(self, group_list):
"""One TSgroup has been deleted on server, so remove its model"""
TSgroup.objects.create(ts_group_name='allowed', ts_group_id=1)
TSgroup.objects.create(ts_group_name='also allowed', ts_group_id=2)
group_list.return_value = {'allowed': '1'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
class MockRequest:
pass
class MockSuperUser:
def has_perm(self, perm, obj=None):
return True
request = MockRequest()
request.user = MockSuperUser()
class Teamspeak3AdminTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.site = AdminSite()
cls.admin = AuthTSgroupAdmin(AuthTS, cls.site)
cls.group = Group.objects.create(name='test')
cls.ts_group = TSgroup.objects.create(ts_group_name='test')
def test_field_queryset_no_reserved_names(self):
"""Ensure all groups are listed when no reserved names"""
form = self.admin.get_form(request)
self.assertQuerysetEqual(form.base_fields['auth_group']._get_queryset(), Group.objects.all())
self.assertQuerysetEqual(form.base_fields['ts_group']._get_queryset(), TSgroup.objects.all())
def test_field_queryset_reserved_names(self):
"""Ensure reserved group names are filtered out"""
ReservedGroupName.objects.bulk_create([ReservedGroupName(name='test', reason='tests', created_by='Bob')])
form = self.admin.get_form(request)
self.assertQuerysetEqual(form.base_fields['auth_group']._get_queryset(), Group.objects.none())
self.assertQuerysetEqual(form.base_fields['ts_group']._get_queryset(), TSgroup.objects.none())

View File

@@ -267,7 +267,9 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
"targets": [4, 5], "targets": [4, 5],
"type": "num" "type": "num"
} }
] ],
"stateSave": true,
"stateDuration": 0
}); });
// tooltip // tooltip

View File

@@ -95,6 +95,11 @@ ul.list-group.list-group-horizontal > li.list-group-item {
.table-aa > tbody > tr:last-child { .table-aa > tbody > tr:last-child {
border-bottom: none; border-bottom: none;
} }
.task-status-progress-bar {
font-size: 15px!important;
line-height: normal!important;
}
} }
/* highlight active menu items /* highlight active menu items

View File

@@ -1,61 +1,20 @@
$(document).ready(function () { $(document).ready(function () {
'use strict'; 'use strict';
/**
* check time
* @param i
* @returns {string}
*/
let checkTime = function (i) {
if (i < 10) {
i = '0' + i;
}
return i;
};
/** /**
* render a JS clock for Eve Time * render a JS clock for Eve Time
* @param element * @param element
* @param utcOffset
*/ */
let renderClock = function (element, utcOffset) { const renderClock = function (element) {
let today = new Date(); const datetimeNow = new Date();
let h = today.getUTCHours(); const h = String(datetimeNow.getUTCHours()).padStart(2, '0');
let m = today.getUTCMinutes(); const m = String(datetimeNow.getUTCMinutes()).padStart(2, '0');
let s = today.getUTCSeconds();
h = h + utcOffset; element.html(h + ':' + m);
if (h > 24) {
h = h - 24;
}
if (h < 0) {
h = h + 24;
}
h = checkTime(h);
m = checkTime(m);
s = checkTime(s);
// document.getElementById('clock').innerHTML = h + ":" + m + ":" + s;
element.html(h + ':' + m + ':' + s);
setTimeout(function () {
renderClock(element, 0);
}, 500);
}; };
/** // Start the Eve time clock in the top menu bar
* functions that need to be executed on load setInterval(function () {
*/ renderClock($('.eve-time-wrapper .eve-time-clock'));
let init = function () { }, 500);
renderClock($('.eve-time-wrapper .eve-time-clock'), 0);
};
/**
* start the show
*/
init();
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
{% load humanize %}
<div
class="progress-bar progress-bar-{{ level }} task-status-progress-bar"
role="progressbar"
aria-valuenow="{% widthratio tasks_count tasks_total 100 %}"
aria-valuemin="0"
aria-valuemax="100"
style="width: {% widthratio tasks_count tasks_total 100 %}%;">
{% widthratio tasks_count tasks_total 100 %}%
</div>

View File

@@ -1,4 +1,6 @@
{% load i18n %} {% load i18n %}
{% load humanize %}
<div class="col-sm-12"> <div class="col-sm-12">
<div class="row vertical-flexbox-row2"> <div class="row vertical-flexbox-row2">
<div class="col-sm-6"> <div class="col-sm-6">
@@ -75,29 +77,25 @@
<div class="panel panel-primary" style="height:50%;"> <div class="panel panel-primary" style="height:50%;">
<div class="panel-heading text-center"><h3 class="panel-title">{% translate "Task Queue" %}</h3></div> <div class="panel-heading text-center"><h3 class="panel-title">{% translate "Task Queue" %}</h3></div>
<div class="panel-body flex-center-horizontal"> <div class="panel-body flex-center-horizontal">
<div class="progress" style="height: 21px;"> <p>
<div class="progress-bar {% blocktranslate with total=tasks_total|intcomma latest=earliest_task|timesince|default:"?" %}
{% if task_queue_length > 500 %} Status of {{ total }} processed tasks • last {{ latest }}
progress-bar-danger {% endblocktranslate %}
{% elif task_queue_length > 100 %} </p>
progress-bar-warning <div
{% else %} class="progress"
progress-bar-success style="height: 21px;"
{% endif %} title="{{ tasks_succeeded|intcomma }} succeeded, {{ tasks_retried|intcomma }} retried, {{ tasks_failed|intcomma }} failed"
" role="progressbar" aria-valuenow="{% widthratio task_queue_length 500 100 %}" >
aria-valuemin="0" aria-valuemax="100" {% include "allianceauth/admin-status/celery_bar_partial.html" with label="suceeded" level="success" tasks_count=tasks_succeeded %}
style="width: {% widthratio task_queue_length 500 100 %}%;"> {% include "allianceauth/admin-status/celery_bar_partial.html" with label="retried" level="info" tasks_count=tasks_retried %}
</div> {% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}
</div> </div>
{% if task_queue_length < 0 %} <p>
{% translate "Error retrieving task queue length" %} {% blocktranslate with queue_length=task_queue_length|default_if_none:"?"|intcomma %}
{% else %} {{ queue_length }} queued tasks
{% blocktrans trimmed count tasks=task_queue_length %} {% endblocktranslate %}
{{ tasks }} task </p>
{% plural %}
{{ tasks }} tasks
{% endblocktrans %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
{% elif message.level_tag == "warning" %} {% elif message.level_tag == "warning" %}
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle"></i>
{% elif message.level_tag == "danger" %} {% elif "danger" in message.level_tag %}
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
{% endif %} {% endif %}
</div> </div>

View File

@@ -0,0 +1,5 @@
{% load static %}
<!-- Start jQuery UI CSS from Alliance Auth -->
<!-- CDNs all contain theme.css, which is not supposed to be in the base CSS, Which is why this is uniquely bundled in not using a CDN -->
<link rel="stylesheet" href="{% static 'js/jquery-ui/1.12.1/css/jquery-ui.min.css' %}" integrity="sha512-7smZe1765O+Mm1UZH46SzaFClbRX7dEs01lB9lqU91oocmugWWfQXVQNVr5tEwktYSqwJMErEfr4GvflXMgTPA==" crossorigin="anonymous" referrerpolicy="no-referrer"/>
<!-- End jQuery UI CSS from aa-gdpr -->

View File

@@ -0,0 +1,3 @@
<!-- Start jQuery UI JS from cdnjs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js" integrity="sha512-uto9mlQzrs59VwILcLiRYeLKPPbS/bT71da/OEBYEwcdNUk8jYIy+D176RYoop1Da+f9mvkYrmj5MCLZWEtQuA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- End jQuery UI JS from cdnjs -->

View File

@@ -1,9 +1,11 @@
import logging import logging
from typing import Optional
import requests
import amqp.exceptions import amqp.exceptions
from packaging.version import Version as Pep440Version, InvalidVersion import requests
from celery.app import app_or_default from celery.app import app_or_default
from packaging.version import InvalidVersion
from packaging.version import Version as Pep440Version
from django import template from django import template
from django.conf import settings from django.conf import settings
@@ -11,6 +13,7 @@ from django.core.cache import cache
from allianceauth import __version__ from allianceauth import __version__
from ..authentication.task_statistics.counters import dashboard_results
register = template.Library() register = template.Library()
@@ -36,30 +39,51 @@ logger = logging.getLogger(__name__)
@register.inclusion_tag('allianceauth/admin-status/overview.html') @register.inclusion_tag('allianceauth/admin-status/overview.html')
def status_overview() -> dict: def status_overview() -> dict:
response = { response = {
'notifications': list(), "notifications": list(),
'current_version': __version__, "current_version": __version__,
'task_queue_length': -1, "task_queue_length": None,
"tasks_succeeded": 0,
"tasks_retried": 0,
"tasks_failed": 0,
"tasks_total": 0,
"tasks_hours": 0,
"earliest_task": None
} }
response.update(_current_notifications()) response.update(_current_notifications())
response.update(_current_version_summary()) response.update(_current_version_summary())
response.update({'task_queue_length': _fetch_celery_queue_length()}) response.update({'task_queue_length': _fetch_celery_queue_length()})
response.update(_celery_stats())
return response return response
def _fetch_celery_queue_length() -> int: def _celery_stats() -> dict:
hours = getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASKS_MAX_HOURS", 24)
results = dashboard_results(hours=hours)
return {
"tasks_succeeded": results.succeeded,
"tasks_retried": results.retried,
"tasks_failed": results.failed,
"tasks_total": results.total,
"tasks_hours": results.hours,
"earliest_task": results.earliest_task
}
def _fetch_celery_queue_length() -> Optional[int]:
try: try:
app = app_or_default(None) app = app_or_default(None)
with app.connection_or_acquire() as conn: with app.connection_or_acquire() as conn:
return conn.default_channel.queue_declare( result = conn.default_channel.queue_declare(
queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'), queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'),
passive=True passive=True
).message_count )
return result.message_count
except amqp.exceptions.ChannelError: except amqp.exceptions.ChannelError:
# Queue doesn't exist, probably empty # Queue doesn't exist, probably empty
return 0 return 0
except Exception: except Exception:
logger.exception("Failed to get celery queue length") logger.exception("Failed to get celery queue length")
return -1 return None
def _current_notifications() -> dict: def _current_notifications() -> dict:
@@ -156,14 +180,27 @@ def _latests_versions(tags: list) -> tuple:
def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list:
"""returns a list from the GitLab API. Supports pageing""" """returns a list from the GitLab API. Supports paging"""
result = list() result = list()
for page in range(1, max_pages + 1): for page in range(1, max_pages + 1):
request = requests.get( try:
url, params={'page': page}, timeout=REQUESTS_TIMEOUT request = requests.get(
) url, params={'page': page}, timeout=REQUESTS_TIMEOUT
request.raise_for_status() )
request.raise_for_status()
except requests.exceptions.RequestException as e:
error_str = str(e)
logger.warning(
f'Unable to fetch from GitLab API. Error: {error_str}',
exc_info=True,
)
return result
result += request.json() result += request.json()
if 'x-total-pages' in request.headers: if 'x-total-pages' in request.headers:
try: try:
total_pages = int(request.headers['x-total-pages']) total_pages = int(request.headers['x-total-pages'])

View File

@@ -174,7 +174,7 @@ class AuthUtils:
alliance_id=None, alliance_id=None,
alliance_name='', alliance_name='',
disconnect_signals=False disconnect_signals=False
): ) -> EveCharacter:
"""new version that works in all cases""" """new version that works in all cases"""
if disconnect_signals: if disconnect_signals:
cls.disconnect_signals() cls.disconnect_signals()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 303 KiB

32
docker/.env.example Normal file
View File

@@ -0,0 +1,32 @@
PROTOCOL=https://
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
DOMAIN=%DOMAIN%
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v2.11
# Nginx Proxy Manager
PROXY_HTTP_PORT=80
PROXY_HTTPS_PORT=443
PROXY_DASH_PORT=81
PROXY_MYSQL_PASS=%PROXY_MYSQL_PASS%
PROXY_MYSQL_PASS_ROOT=%PROXY_MYSQL_PASS_ROOT%
# grafana
GRAFANA_DB_PASSWORD=%GRAFANA_DB_PASSWORD%
# Alliance Auth Environment Config
AA_SITENAME=%AA_SITENAME%
AA_SECRET_KEY=%AA_SECRET_KEY%
AA_DB_HOST=auth_mysql
AA_DB_NAME=alliance_auth
AA_DB_USER=aauth
AA_DB_PASSWORD=%AA_DB_PASSWORD%
AA_DB_ROOT_PASSWORD=%AA_DB_ROOT_PASSWORD%
AA_EMAIL_HOST=''
AA_EMAIL_PORT=587
AA_EMAIL_HOST_USER=''
AA_EMAIL_HOST_PASSWORD=''
AA_EMAIL_USE_TLS=True
AA_DEFAULT_FROM_EMAIL=''
ESI_SSO_CLIENT_ID=%ESI_SSO_CLIENT_ID%
ESI_SSO_CLIENT_SECRET=%ESI_SSO_CLIENT_SECRET%
ESI_USER_CONTACT_EMAIL=%ESI_USER_CONTACT_EMAIL%

2
docker/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
setup.sql

48
docker/Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
FROM python:3.9-slim
ARG AUTH_VERSION=2.11.1
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
ENV VIRTUAL_ENV=/opt/venv
ENV AUTH_USER=allianceauth
ENV AUTH_GROUP=allianceauth
ENV AUTH_USERGROUP=${AUTH_USER}:${AUTH_GROUP}
ENV STATIC_BASE=/var/www
ENV AUTH_HOME=/home/allianceauth
# Setup user and directory permissions
SHELL ["/bin/bash", "-c"]
RUN groupadd -g 61000 ${AUTH_GROUP}
RUN useradd -g 61000 -l -M -s /bin/false -u 61000 ${AUTH_USER}
RUN mkdir -p ${VIRTUAL_ENV} \
&& chown ${AUTH_USERGROUP} ${VIRTUAL_ENV} \
&& mkdir -p ${STATIC_BASE} \
&& chown ${AUTH_USERGROUP} ${STATIC_BASE} \
&& mkdir -p ${AUTH_HOME} \
&& chown ${AUTH_USERGROUP} ${AUTH_HOME}
# Install build dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
libmariadb-dev gcc supervisor git htop
# Switch to non-root user
USER ${AUTH_USER}
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR ${AUTH_HOME}
# Install python dependencies
RUN pip install --upgrade pip
RUN pip install wheel gunicorn
RUN pip install ${AUTH_PACKAGE}
# Initialize auth
RUN allianceauth start myauth
COPY /allianceauth/project_template/project_name/settings/local.py ${AUTH_HOME}/myauth/myauth/settings/local.py
RUN allianceauth update myauth
RUN mkdir -p ${STATIC_BASE}/myauth/static
COPY /docker/conf/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN echo 'alias auth="python $AUTH_HOME/myauth/manage.py"' >> ~/.bashrc && \
echo 'alias supervisord="supervisord -c /etc/supervisor/conf.d/supervisord.conf"' >> ~/.bashrc && \
source ~/.bashrc
EXPOSE 8000
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

82
docker/README.md Normal file
View File

@@ -0,0 +1,82 @@
# Alliance Auth -- Docker
## Prerequesites
You should have the following available on the system you are using to set this up:
* Docker - https://docs.docker.com/get-docker/
* git
* curl
## Setup Guide
1. run `bash <(curl -s https://gitlab.com/allianceauth/allianceauth/-/raw/v2.11.x/docker/scripts/download.sh)`. This will download all the files you need to install auth and place them in a directory named `aa-docker`. Feel free to rename/move this folder.
1. run `./scripts/prepare-env.sh` to set up your environment
1. (optional) Change `PROTOCOL` to `http://` if not using SSL in `.env`
1. run `docker-compose --env-file=.env up -d` (NOTE: if this command hangs, follow the instructions [here](https://www.digitalocean.com/community/tutorials/how-to-setup-additional-entropy-for-cloud-servers-using-haveged))
1. run `docker-compose exec allianceauth bash` to open up a terminal inside your auth container
1. run `auth migrate`
1. run `auth collectstatic`
1. run `auth createsuperuser`
1. visit http://yourdomain:81 to set up nginx proxy manager (NOTE: if this doesn't work, the machine likely has a firewall. You'll want to open up ports 80,443, and 81. [Instructions for ufw](https://www.digitalocean.com/community/tutorials/ufw-essentials-common-firewall-rules-and-commands))
1. login with user `admin@example.com` and password `changeme`, then update your password as requested
1. click on "Proxy Hosts"
1. click "Add Proxy Host", with the following settings for auth. The example uses `auth.localhost` for the domain, but you'll want to use whatever address you have auth configured on
![](./docs/images/nginx-host.png)
1. click "Add Proxy Host", with the following settings for grafana. The example uses `grafana.localhost` for the domain
![](./docs/images/grafana-host.png)
Congrats! You should now see auth running at http://auth.yourdomain and grafana at http://grafana.yourdomain!
## SSL Guide
Unless you're running auth locally in docker for testing, you should be using SSL.
Thankfully, setting up SSL in nginx Proxy Manager takes about three clicks.
1. Edit your existing proxy host, and go to the SSL tab. Select "Request a new SSL Certificate" from the drop down.
1. Now, enable "Force SSL" and "HTTP/2 Support". (NOTE: Do not enable HSTS unless you know what you're doing. This will force your domains to only work with SSL enabled, and is cached extremely hard in browsers. )
![](./docs/images/proxy-manager-ssl.png)
1. (optional) select "Use a DNS Challenge". This is not a required option, but it is recommended if you use a supported DNS provider. You'll then be asked for an API key for the provider you choose. If you use Cloudflare, you'll probably have issues getting SSL certs unless you use a DNS Challenge.
1. The email address here will be used to notify you if there are issues renewing your certificates.
1. Repeat for any other services, like grafana.
That's it! You should now be able to access your auth install at https://auth.yourdomain
## Adding extra packages
There are a handful of ways to add packages:
* Running `pip install` in the container
* Modifying the container's initial command to install packages
* Building a custom Docker image (recommended, and less scary than it sounds!)
### Using a custom docker image
Using a custom docker image is the preferred approach, as it gives you the stability of packages only changing when you tell them to, along with packages not having to be downloaded every time your container restarts
1. Add each additional package that you want to install to a single line in `conf/requirements.txt`. It is recommended, but not required, that you include a version number as well. This will keep your packages from magically updating. You can lookup packages on https://package.wiki, and copy everything after `pip install` from the top of the page to use the most recent version. It should look something like `allianceauth-signal-pings==0.0.7`. Every entry in this file should be on a separate line
1. In `docker-compose.yml`, comment out the `image` line under `allianceauth` (line 36... ish) and uncomment the `build` section
1. Now run `docker-compose --env-file=.env up -d`, your custom container will be built, and auth will have your new packages. Make sure to follow the package's instructions on config values that go in `local.py`
_NOTE: It is recommended that you put any secret values (API keys, database credentials, etc) in an environment variable instead of hardcoding them into `local.py`. This gives you the ability to track your config in git without committing passwords. To do this, just add it to your `.env` file, and then reference in `local.py` with `os.environ.get("SECRET_NAME")`_
## Updating Auth
### Base Image
Whether you're using a custom image or not, the version of auth is dictated by $AA_DOCKER_TAG in your `.env` file.
1. To update to a new version of auth, update the version number at the end (or replace the whole value with the tag in the release notes).
1. Next, run `docker-compose pull`
1. Finally, run `docker-compose --env-file=.env up -d`
_NOTE: If you specify a version of allianceauth in your `requirements.txt` in a custom image it will override the version from the base image. Not recommended unless you know what you're doing_
### Custom Packages
1. Update the versions in your `requirements.txt` file
1. Run `docker-compose build`
1. Run `docker-compose --env-file=.env up -d`
## Notes
### Apple M1 Support
If you want to run locally on an M1 powered Apple device, you'll need to add `platform: linux/x86_64` under each container in `docker-compose.yml` as the auth container is not compiled for ARM (other containers may work without this, but it's known to work if added to all containers).
Example:
```yaml
redis:
platform: linux/x86_64
image: redis:6.2
```

81
docker/conf/local.py Normal file
View File

@@ -0,0 +1,81 @@
# Every setting in base.py can be overloaded by redefining it here.
from .base import *
SECRET_KEY = os.environ.get("AA_SECRET_KEY")
SITE_NAME = os.environ.get("AA_SITENAME")
DEBUG = os.environ.get("AA_DEBUG", False)
DATABASES['default'] = {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get("AA_DB_NAME"),
'USER': os.environ.get("AA_DB_USER"),
'PASSWORD': os.environ.get("AA_DB_PASSWORD"),
'HOST': os.environ.get("AA_DB_HOST"),
'PORT': os.environ.get("AA_DB_PORT", "3306"),
}
# Register an application at https://developers.eveonline.com for Authentication
# & API Access and fill out these settings. Be sure to set the callback URL
# to https://example.com/sso/callback substituting your domain for example.com
# Logging in to auth requires the publicData scope (can be overridden through the
# LOGIN_TOKEN_SCOPES setting). Other apps may require more (see their docs).
ESI_SSO_CLIENT_ID = os.environ.get("ESI_SSO_CLIENT_ID")
ESI_SSO_CLIENT_SECRET = os.environ.get("ESI_SSO_CLIENT_SECRET")
ESI_SSO_CALLBACK_URL = (f"{os.environ.get('PROTOCOL')}"
f"{os.environ.get('AUTH_SUBDOMAIN')}."
f"{os.environ.get('DOMAIN')}/sso/callback")
ESI_USER_CONTACT_EMAIL = os.environ.get("ESI_USER_CONTACT_EMAIL") # A server maintainer that CCP can contact in case of issues.
# By default emails are validated before new users can log in.
# It's recommended to use a free service like SparkPost or Elastic Email to send email.
# https://www.sparkpost.com/docs/integrations/django/
# https://elasticemail.com/resources/settings/smtp-api/
# Set the default from email to something like 'noreply@example.com'
# Email validation can be turned off by uncommenting the line below. This can break some services.
REGISTRATION_VERIFY_EMAIL = False
EMAIL_HOST = os.environ.get("AA_EMAIL_HOST", "")
EMAIL_PORT = os.environ.get("AA_EMAIL_PORT", 587)
EMAIL_HOST_USER = os.environ.get("AA_EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("AA_EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = os.environ.get("AA_EMAIL_USE_TLS", True)
DEFAULT_FROM_EMAIL = os.environ.get("AA_DEFAULT_FROM_EMAIL", "")
ROOT_URLCONF = 'myauth.urls'
WSGI_APPLICATION = 'myauth.wsgi.application'
STATIC_ROOT = "/var/www/myauth/static/"
BROKER_URL = F"redis://{os.environ.get('AA_REDIS', 'redis:6379')}/0"
CELERY_RESULT_BACKEND = F"redis://{os.environ.get('AA_REDIS', 'redis:6379')}/0"
CACHES = {
"default": {
"BACKEND": "redis_cache.RedisCache",
"LOCATION": os.environ.get('AA_REDIS', 'redis:6379'),
"OPTIONS": {
"DB": 1,
}
}
}
# Add any additional apps to this list.
INSTALLED_APPS += [
# https://allianceauth.readthedocs.io/en/latest/features/apps/index.html
# 'allianceauth.corputils',
# 'allianceauth.fleetactivitytracking',
# 'allianceauth.optimer',
# 'allianceauth.permissions_tool',
# 'allianceauth.srp',
# 'allianceauth.timerboard',
# https://allianceauth.readthedocs.io/en/latest/features/services/index.html
# 'allianceauth.services.modules.discord',
# 'allianceauth.services.modules.discourse',
# 'allianceauth.services.modules.ips4',
# 'allianceauth.services.modules.openfire',
# 'allianceauth.services.modules.phpbb3',
# 'allianceauth.services.modules.smf',
# 'allianceauth.services.modules.teamspeak3',
# 'allianceauth.services.modules.xenforo',
]
#######################################
# Add any custom settings below here. #
#######################################

20
docker/conf/nginx.conf Normal file
View File

@@ -0,0 +1,20 @@
server {
listen 80;
location = /favicon.ico { access_log off; log_not_found off; }
location /static {
alias /var/www/myauth/static;
autoindex off;
}
location /robots.txt {
alias /var/www/myauth/static/robots.txt;
}
location / {
proxy_pass http://allianceauth:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
}
}

View File

View File

@@ -0,0 +1,56 @@
[supervisord]
nodaemon=true
user=allianceauth
[program:beat]
command=/opt/venv/bin/celery -A myauth beat
directory=/home/allianceauth/myauth
user=allianceauth
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startsecs=10
priority=998
stdout_events_enabled=true
stderr_events_enabled=true
[program:worker]
command=/opt/venv/bin/celery -A myauth worker -l INFO --max-tasks-per-child=250
directory=/home/allianceauth/myauth
user=allianceauth
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
numprocs=1
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998
stdout_events_enabled=true
stderr_events_enabled=true
[program:gunicorn]
user=allianceauth
directory=/home/allianceauth/myauth
command=/opt/venv/bin/gunicorn myauth.wsgi --bind :8000 --workers=3 --timeout 120
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
stdout_events_enabled=true
stderr_events_enabled=true
autostart=true
autorestart=true
stopsignal=INT
[group:myauth]
programs=beat,worker,gunicorn
priority=999
[supervisorctl]

8
docker/custom.dockerfile Normal file
View File

@@ -0,0 +1,8 @@
ARG AA_DOCKER_TAG
FROM $AA_DOCKER_TAG
RUN cd /home/allianceauth
COPY /conf/requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN python $AUTH_HOME/myauth/manage.py collectstatic --noinput
RUN allianceauth update myauth

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