mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-05 06:36:19 +01:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e9f5e6430 | ||
|
|
ceaa064e62 | ||
|
|
1aad3e4512 | ||
|
|
f83c3c2811 | ||
|
|
a23ec6d318 | ||
|
|
ecc53888bc | ||
|
|
e54f72091f | ||
|
|
75b5b28804 | ||
|
|
f81a2ed237 | ||
|
|
49e01157e7 | ||
|
|
28420a729e | ||
|
|
52a4cf8d52 | ||
|
|
703c2392a9 | ||
|
|
18c9a66437 | ||
|
|
9687d57de9 | ||
|
|
60c2e57d83 | ||
|
|
b14bff0145 | ||
|
|
9166886665 | ||
|
|
c74010d441 | ||
|
|
640a21e4db | ||
|
|
fd442a5735 | ||
|
|
c7b99044bc | ||
|
|
234451a7d4 | ||
|
|
ffff904ab1 | ||
|
|
d71a26220c | ||
|
|
beeeb8dc5d | ||
|
|
19244cc4c6 | ||
|
|
cc94ba6b5e | ||
|
|
c9926cc877 | ||
|
|
1d14e1b0af | ||
|
|
297da44a5a | ||
|
|
402ff53a5c | ||
|
|
2d6e4a0df1 | ||
|
|
defcfa3316 | ||
|
|
3209b71b0a | ||
|
|
80b3ca0a1e | ||
|
|
8351bd2fa3 | ||
|
|
255966ed3b | ||
|
|
8d6ebf4770 | ||
|
|
2ca752bf78 | ||
|
|
79e1192f67 | ||
|
|
ff610efc84 | ||
|
|
6b68a739ef | ||
|
|
909bd0ba15 | ||
|
|
05110abc59 | ||
|
|
a64d99eb91 | ||
|
|
0e45403195 | ||
|
|
e16a9ffe65 | ||
|
|
57de122ef8 |
@@ -14,6 +14,7 @@ stages:
|
|||||||
include:
|
include:
|
||||||
- template: Dependency-Scanning.gitlab-ci.yml
|
- template: Dependency-Scanning.gitlab-ci.yml
|
||||||
- template: Security/SAST.gitlab-ci.yml
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
|
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install redis-server -y
|
- apt-get update && apt-get install redis-server -y
|
||||||
@@ -24,7 +25,7 @@ before_script:
|
|||||||
pre-commit-check:
|
pre-commit-check:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
stage: pre-commit
|
stage: pre-commit
|
||||||
image: python:3.6-buster
|
image: python:3.8-bullseye
|
||||||
variables:
|
variables:
|
||||||
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||||
cache:
|
cache:
|
||||||
@@ -46,18 +47,6 @@ dependency_scanning:
|
|||||||
- python -V
|
- python -V
|
||||||
- pip install wheel tox
|
- pip install wheel tox
|
||||||
|
|
||||||
test-3.7-core:
|
|
||||||
<<: *only-default
|
|
||||||
image: python:3.7-bullseye
|
|
||||||
script:
|
|
||||||
- tox -e py37-core
|
|
||||||
artifacts:
|
|
||||||
when: always
|
|
||||||
reports:
|
|
||||||
coverage_report:
|
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.8-core:
|
test-3.8-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.8-bullseye
|
image: python:3.8-bullseye
|
||||||
@@ -66,9 +55,7 @@ test-3.8-core:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.9-core:
|
test-3.9-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
@@ -78,9 +65,7 @@ test-3.9-core:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.10-core:
|
test-3.10-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
@@ -90,9 +75,7 @@ test-3.10-core:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.11-core:
|
test-3.11-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
@@ -102,23 +85,9 @@ test-3.11-core:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
test-3.7-all:
|
|
||||||
<<: *only-default
|
|
||||||
image: python:3.7-bullseye
|
|
||||||
script:
|
|
||||||
- tox -e py37-all
|
|
||||||
artifacts:
|
|
||||||
when: always
|
|
||||||
reports:
|
|
||||||
coverage_report:
|
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.8-all:
|
test-3.8-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.8-bullseye
|
image: python:3.8-bullseye
|
||||||
@@ -127,9 +96,7 @@ test-3.8-all:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.9-all:
|
test-3.9-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
@@ -139,9 +106,7 @@ test-3.9-all:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.10-all:
|
test-3.10-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
@@ -151,9 +116,7 @@ test-3.10-all:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.11-all:
|
test-3.11-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
@@ -163,17 +126,9 @@ test-3.11-all:
|
|||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
coverage_report:
|
cobertura: coverage.xml
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
test-docs:
|
|
||||||
<<: *only-default
|
|
||||||
image: python:3.9-bullseye
|
|
||||||
script:
|
|
||||||
- tox -e docs
|
|
||||||
|
|
||||||
deploy_production:
|
deploy_production:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: python:3.10-bullseye
|
image: python:3.10-bullseye
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.0.1
|
rev: v4.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-json
|
- id: check-json
|
||||||
@@ -22,13 +22,13 @@ repos:
|
|||||||
args: [ '--remove' ]
|
args: [ '--remove' ]
|
||||||
|
|
||||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||||
rev: 2.3.54
|
rev: 2.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: editorconfig-checker
|
- id: editorconfig-checker
|
||||||
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
|
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.29.0
|
rev: v2.30.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [ --py37-plus ]
|
args: [ --py38-plus ]
|
||||||
|
|||||||
@@ -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.15.1'
|
__version__ = '3.0.0a1'
|
||||||
__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__}'
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ from urllib.parse import parse_qs
|
|||||||
|
|
||||||
import requests_mock
|
import requests_mock
|
||||||
|
|
||||||
from django.test import override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from allianceauth.analytics.tasks import ANALYTICS_URL
|
from allianceauth.analytics.tasks import ANALYTICS_URL
|
||||||
from allianceauth.eveonline.tasks import update_character
|
from allianceauth.eveonline.tasks import update_character
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
class TestAnalyticsForViews(NoSocketsTestCase):
|
class TestAnalyticsForViews(TestCase):
|
||||||
@override_settings(ANALYTICS_DISABLED=False)
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
def test_should_run_analytics(self, requests_mocker):
|
def test_should_run_analytics(self, requests_mocker):
|
||||||
# given
|
# given
|
||||||
@@ -41,7 +40,7 @@ class TestAnalyticsForViews(NoSocketsTestCase):
|
|||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
class TestAnalyticsForTasks(NoSocketsTestCase):
|
class TestAnalyticsForTasks(TestCase):
|
||||||
@override_settings(ANALYTICS_DISABLED=False)
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||||
def test_should_run_analytics_for_successful_task(
|
def test_should_run_analytics_for_successful_task(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from allianceauth.analytics.middleware import AnalyticsMiddleware
|
from allianceauth.analytics.middleware import AnalyticsMiddleware
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from django.test.testcases import TestCase
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ from django.test.testcases import TestCase
|
|||||||
class TestAnalyticsMiddleware(TestCase):
|
class TestAnalyticsMiddleware(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.middleware = AnalyticsMiddleware()
|
self.middleware = AnalyticsMiddleware(HttpResponse)
|
||||||
self.request = Mock()
|
self.request = Mock()
|
||||||
self.request.headers = {
|
self.request.headers = {
|
||||||
"User-Agent": "AUTOMATED TEST"
|
"User-Agent": "AUTOMATED TEST"
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
import requests_mock
|
|
||||||
|
|
||||||
from django.test.utils import override_settings
|
|
||||||
|
|
||||||
from allianceauth.analytics.tasks import (
|
from allianceauth.analytics.tasks import (
|
||||||
analytics_event,
|
analytics_event,
|
||||||
send_ga_tracking_celery_event,
|
send_ga_tracking_celery_event,
|
||||||
send_ga_tracking_web_view)
|
send_ga_tracking_web_view)
|
||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
|
||||||
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/collect'
|
class TestAnalyticsTasks(TestCase):
|
||||||
|
def test_analytics_event(self):
|
||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
|
||||||
@requests_mock.Mocker()
|
|
||||||
class TestAnalyticsTasks(NoSocketsTestCase):
|
|
||||||
def test_analytics_event(self, requests_mocker):
|
|
||||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
|
||||||
analytics_event(
|
analytics_event(
|
||||||
category='allianceauth.analytics',
|
category='allianceauth.analytics',
|
||||||
action='send_tests',
|
action='send_tests',
|
||||||
@@ -24,19 +14,15 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
value=1,
|
value=1,
|
||||||
event_type='Stats')
|
event_type='Stats')
|
||||||
|
|
||||||
def test_send_ga_tracking_web_view_sent(self, requests_mocker):
|
def test_send_ga_tracking_web_view_sent(self):
|
||||||
"""This test sends if the event SENDS to google.
|
# This test sends if the event SENDS to google
|
||||||
Not if it was successful.
|
# Not if it was successful
|
||||||
"""
|
|
||||||
# given
|
|
||||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
page = '/index/'
|
page = '/index/'
|
||||||
title = 'Hello World'
|
title = 'Hello World'
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
# when
|
|
||||||
response = send_ga_tracking_web_view(
|
response = send_ga_tracking_web_view(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -44,23 +30,15 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
title,
|
title,
|
||||||
locale,
|
locale,
|
||||||
useragent)
|
useragent)
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_send_ga_tracking_web_view_success(self, requests_mocker):
|
def test_send_ga_tracking_web_view_success(self):
|
||||||
# given
|
|
||||||
requests_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
|
||||||
json={"hitParsingResult":[{'valid': True}]}
|
|
||||||
)
|
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
page = '/index/'
|
page = '/index/'
|
||||||
title = 'Hello World'
|
title = 'Hello World'
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
# when
|
|
||||||
json_response = send_ga_tracking_web_view(
|
json_response = send_ga_tracking_web_view(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -68,42 +46,15 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
title,
|
title,
|
||||||
locale,
|
locale,
|
||||||
useragent).json()
|
useragent).json()
|
||||||
# then
|
|
||||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
|
||||||
def test_send_ga_tracking_web_view_invalid_token(self, requests_mocker):
|
def test_send_ga_tracking_web_view_invalid_token(self):
|
||||||
# given
|
|
||||||
requests_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
|
||||||
json={
|
|
||||||
"hitParsingResult":[
|
|
||||||
{
|
|
||||||
'valid': False,
|
|
||||||
'parserMessage': [
|
|
||||||
{
|
|
||||||
'messageType': 'INFO',
|
|
||||||
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
|
|
||||||
'messageCode': 'VALUE_MODIFIED'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'messageType': 'ERROR',
|
|
||||||
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
|
|
||||||
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
page = '/index/'
|
page = '/index/'
|
||||||
title = 'Hello World'
|
title = 'Hello World'
|
||||||
locale = 'en'
|
locale = 'en'
|
||||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
# when
|
|
||||||
json_response = send_ga_tracking_web_view(
|
json_response = send_ga_tracking_web_view(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -111,25 +62,18 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
title,
|
title,
|
||||||
locale,
|
locale,
|
||||||
useragent).json()
|
useragent).json()
|
||||||
# then
|
|
||||||
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||||
self.assertEqual(
|
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
||||||
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
|
|
||||||
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
|
|
||||||
)
|
|
||||||
|
|
||||||
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||||
|
|
||||||
def test_send_ga_tracking_celery_event_sent(self, requests_mocker):
|
def test_send_ga_tracking_celery_event_sent(self):
|
||||||
# given
|
|
||||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
category = 'test'
|
category = 'test'
|
||||||
action = 'test'
|
action = 'test'
|
||||||
label = 'test'
|
label = 'test'
|
||||||
value = '1'
|
value = '1'
|
||||||
# when
|
|
||||||
response = send_ga_tracking_celery_event(
|
response = send_ga_tracking_celery_event(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -137,23 +81,15 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
value)
|
value)
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_send_ga_tracking_celery_event_success(self, requests_mocker):
|
def test_send_ga_tracking_celery_event_success(self):
|
||||||
# given
|
|
||||||
requests_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
|
||||||
json={"hitParsingResult":[{'valid': True}]}
|
|
||||||
)
|
|
||||||
tracking_id = 'UA-186249766-2'
|
tracking_id = 'UA-186249766-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
category = 'test'
|
category = 'test'
|
||||||
action = 'test'
|
action = 'test'
|
||||||
label = 'test'
|
label = 'test'
|
||||||
value = '1'
|
value = '1'
|
||||||
# when
|
|
||||||
json_response = send_ga_tracking_celery_event(
|
json_response = send_ga_tracking_celery_event(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -161,42 +97,15 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
value).json()
|
value).json()
|
||||||
# then
|
|
||||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
|
||||||
def test_send_ga_tracking_celery_event_invalid_token(self, requests_mocker):
|
def test_send_ga_tracking_celery_event_invalid_token(self):
|
||||||
# given
|
|
||||||
requests_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
|
||||||
json={
|
|
||||||
"hitParsingResult":[
|
|
||||||
{
|
|
||||||
'valid': False,
|
|
||||||
'parserMessage': [
|
|
||||||
{
|
|
||||||
'messageType': 'INFO',
|
|
||||||
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
|
|
||||||
'messageCode': 'VALUE_MODIFIED'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'messageType': 'ERROR',
|
|
||||||
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
|
|
||||||
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
category = 'test'
|
category = 'test'
|
||||||
action = 'test'
|
action = 'test'
|
||||||
label = 'test'
|
label = 'test'
|
||||||
value = '1'
|
value = '1'
|
||||||
# when
|
|
||||||
json_response = send_ga_tracking_celery_event(
|
json_response = send_ga_tracking_celery_event(
|
||||||
tracking_id,
|
tracking_id,
|
||||||
client_id,
|
client_id,
|
||||||
@@ -204,9 +113,7 @@ class TestAnalyticsTasks(NoSocketsTestCase):
|
|||||||
action,
|
action,
|
||||||
label,
|
label,
|
||||||
value).json()
|
value).json()
|
||||||
# then
|
|
||||||
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||||
self.assertEqual(
|
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
||||||
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
|
|
||||||
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
|
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,44 +1,30 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import User as BaseUser, \
|
||||||
from django.contrib.auth.models import Permission as BasePermission
|
Permission as BasePermission, Group
|
||||||
from django.contrib.auth.models import User as BaseUser
|
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
from allianceauth.services.hooks import ServicesHook
|
||||||
|
from django.db.models.signals import pre_save, post_save, pre_delete, \
|
||||||
|
post_delete, m2m_changed
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.db.models.signals import (
|
|
||||||
m2m_changed,
|
|
||||||
post_delete,
|
|
||||||
post_save,
|
|
||||||
pre_delete,
|
|
||||||
pre_save
|
|
||||||
)
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.forms import ModelForm
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from allianceauth.authentication.models import (
|
from allianceauth.authentication.models import (
|
||||||
CharacterOwnership,
|
|
||||||
OwnershipRecord,
|
|
||||||
State,
|
State,
|
||||||
|
get_guest_state,
|
||||||
|
CharacterOwnership,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
get_guest_state
|
OwnershipRecord)
|
||||||
)
|
|
||||||
from allianceauth.eveonline.models import (
|
|
||||||
EveAllianceInfo,
|
|
||||||
EveCharacter,
|
|
||||||
EveCorporationInfo,
|
|
||||||
EveFactionInfo
|
|
||||||
)
|
|
||||||
from allianceauth.eveonline.tasks import update_character
|
|
||||||
from allianceauth.hooks import get_hooks
|
from allianceauth.hooks import get_hooks
|
||||||
from allianceauth.services.hooks import ServicesHook
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
|
||||||
|
EveAllianceInfo, EveFactionInfo
|
||||||
from .app_settings import (
|
from allianceauth.eveonline.tasks import update_character
|
||||||
AUTHENTICATION_ADMIN_USERS_MAX_CHARS,
|
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
|
||||||
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
|
||||||
)
|
|
||||||
from .forms import UserChangeForm, UserProfileForm
|
|
||||||
|
|
||||||
|
|
||||||
def make_service_hooks_update_groups_action(service):
|
def make_service_hooks_update_groups_action(service):
|
||||||
@@ -77,10 +63,19 @@ def make_service_hooks_sync_nickname_action(service):
|
|||||||
return sync_nickname
|
return sync_nickname
|
||||||
|
|
||||||
|
|
||||||
|
class QuerysetModelForm(ModelForm):
|
||||||
|
# allows specifying FK querysets through kwarg
|
||||||
|
def __init__(self, querysets=None, *args, **kwargs):
|
||||||
|
querysets = querysets or {}
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field, qs in querysets.items():
|
||||||
|
self.fields[field].queryset = qs
|
||||||
|
|
||||||
|
|
||||||
class UserProfileInline(admin.StackedInline):
|
class UserProfileInline(admin.StackedInline):
|
||||||
model = UserProfile
|
model = UserProfile
|
||||||
readonly_fields = ('state',)
|
readonly_fields = ('state',)
|
||||||
form = UserProfileForm
|
form = QuerysetModelForm
|
||||||
verbose_name = ''
|
verbose_name = ''
|
||||||
verbose_name_plural = 'Profile'
|
verbose_name_plural = 'Profile'
|
||||||
|
|
||||||
@@ -108,7 +103,6 @@ class UserProfileInline(admin.StackedInline):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@admin.display(description="")
|
|
||||||
def user_profile_pic(obj):
|
def user_profile_pic(obj):
|
||||||
"""profile pic column data for user objects
|
"""profile pic column data for user objects
|
||||||
|
|
||||||
@@ -121,10 +115,13 @@ def user_profile_pic(obj):
|
|||||||
'<img src="{}" class="img-circle">',
|
'<img src="{}" class="img-circle">',
|
||||||
user_obj.profile.main_character.portrait_url(size=32)
|
user_obj.profile.main_character.portrait_url(size=32)
|
||||||
)
|
)
|
||||||
return None
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
user_profile_pic.short_description = ''
|
||||||
|
|
||||||
|
|
||||||
@admin.display(description="user / main", ordering="username")
|
|
||||||
def user_username(obj):
|
def user_username(obj):
|
||||||
"""user column data for user objects
|
"""user column data for user objects
|
||||||
|
|
||||||
@@ -146,17 +143,18 @@ def user_username(obj):
|
|||||||
user_obj.username,
|
user_obj.username,
|
||||||
user_obj.profile.main_character.character_name
|
user_obj.profile.main_character.character_name
|
||||||
)
|
)
|
||||||
return format_html(
|
else:
|
||||||
'<strong><a href="{}">{}</a></strong>',
|
return format_html(
|
||||||
link,
|
'<strong><a href="{}">{}</a></strong>',
|
||||||
user_obj.username,
|
link,
|
||||||
)
|
user_obj.username,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
user_username.short_description = 'user / main'
|
||||||
|
user_username.admin_order_field = 'username'
|
||||||
|
|
||||||
|
|
||||||
@admin.display(
|
|
||||||
description="Corporation / Alliance (Main)",
|
|
||||||
ordering="profile__main_character__corporation_name"
|
|
||||||
)
|
|
||||||
def user_main_organization(obj):
|
def user_main_organization(obj):
|
||||||
"""main organization column data for user objects
|
"""main organization column data for user objects
|
||||||
|
|
||||||
@@ -165,15 +163,21 @@ def user_main_organization(obj):
|
|||||||
"""
|
"""
|
||||||
user_obj = obj.user if hasattr(obj, 'user') else obj
|
user_obj = obj.user if hasattr(obj, 'user') else obj
|
||||||
if not user_obj.profile.main_character:
|
if not user_obj.profile.main_character:
|
||||||
return ''
|
result = ''
|
||||||
result = user_obj.profile.main_character.corporation_name
|
else:
|
||||||
if user_obj.profile.main_character.alliance_id:
|
result = user_obj.profile.main_character.corporation_name
|
||||||
result += f'<br>{user_obj.profile.main_character.alliance_name}'
|
if user_obj.profile.main_character.alliance_id:
|
||||||
elif user_obj.profile.main_character.faction_name:
|
result += f'<br>{user_obj.profile.main_character.alliance_name}'
|
||||||
result += f'<br>{user_obj.profile.main_character.faction_name}'
|
elif user_obj.profile.main_character.faction_name:
|
||||||
|
result += f'<br>{user_obj.profile.main_character.faction_name}'
|
||||||
return format_html(result)
|
return format_html(result)
|
||||||
|
|
||||||
|
|
||||||
|
user_main_organization.short_description = 'Corporation / Alliance (Main)'
|
||||||
|
user_main_organization.admin_order_field = \
|
||||||
|
'profile__main_character__corporation_name'
|
||||||
|
|
||||||
|
|
||||||
class MainCorporationsFilter(admin.SimpleListFilter):
|
class MainCorporationsFilter(admin.SimpleListFilter):
|
||||||
"""Custom filter to filter on corporations from mains only
|
"""Custom filter to filter on corporations from mains only
|
||||||
|
|
||||||
@@ -196,13 +200,15 @@ class MainCorporationsFilter(admin.SimpleListFilter):
|
|||||||
def queryset(self, request, qs):
|
def queryset(self, request, qs):
|
||||||
if self.value() is None:
|
if self.value() is None:
|
||||||
return qs.all()
|
return qs.all()
|
||||||
if qs.model == User:
|
else:
|
||||||
return qs.filter(
|
if qs.model == User:
|
||||||
profile__main_character__corporation_id=self.value()
|
return qs.filter(
|
||||||
)
|
profile__main_character__corporation_id=self.value()
|
||||||
return qs.filter(
|
)
|
||||||
user__profile__main_character__corporation_id=self.value()
|
else:
|
||||||
)
|
return qs.filter(
|
||||||
|
user__profile__main_character__corporation_id=self.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MainAllianceFilter(admin.SimpleListFilter):
|
class MainAllianceFilter(admin.SimpleListFilter):
|
||||||
@@ -215,14 +221,12 @@ class MainAllianceFilter(admin.SimpleListFilter):
|
|||||||
parameter_name = 'main_alliance_id__exact'
|
parameter_name = 'main_alliance_id__exact'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
qs = (
|
qs = EveCharacter.objects\
|
||||||
EveCharacter.objects
|
.exclude(alliance_id=None)\
|
||||||
.exclude(alliance_id=None)
|
.exclude(userprofile=None)\
|
||||||
.exclude(userprofile=None)
|
.values('alliance_id', 'alliance_name')\
|
||||||
.values('alliance_id', 'alliance_name')
|
.distinct()\
|
||||||
.distinct()
|
|
||||||
.order_by(Lower('alliance_name'))
|
.order_by(Lower('alliance_name'))
|
||||||
)
|
|
||||||
return tuple(
|
return tuple(
|
||||||
(x['alliance_id'], x['alliance_name']) for x in qs
|
(x['alliance_id'], x['alliance_name']) for x in qs
|
||||||
)
|
)
|
||||||
@@ -230,11 +234,13 @@ class MainAllianceFilter(admin.SimpleListFilter):
|
|||||||
def queryset(self, request, qs):
|
def queryset(self, request, qs):
|
||||||
if self.value() is None:
|
if self.value() is None:
|
||||||
return qs.all()
|
return qs.all()
|
||||||
if qs.model == User:
|
else:
|
||||||
return qs.filter(profile__main_character__alliance_id=self.value())
|
if qs.model == User:
|
||||||
return qs.filter(
|
return qs.filter(profile__main_character__alliance_id=self.value())
|
||||||
user__profile__main_character__alliance_id=self.value()
|
else:
|
||||||
)
|
return qs.filter(
|
||||||
|
user__profile__main_character__alliance_id=self.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MainFactionFilter(admin.SimpleListFilter):
|
class MainFactionFilter(admin.SimpleListFilter):
|
||||||
@@ -247,14 +253,12 @@ class MainFactionFilter(admin.SimpleListFilter):
|
|||||||
parameter_name = 'main_faction_id__exact'
|
parameter_name = 'main_faction_id__exact'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
qs = (
|
qs = EveCharacter.objects\
|
||||||
EveCharacter.objects
|
.exclude(faction_id=None)\
|
||||||
.exclude(faction_id=None)
|
.exclude(userprofile=None)\
|
||||||
.exclude(userprofile=None)
|
.values('faction_id', 'faction_name')\
|
||||||
.values('faction_id', 'faction_name')
|
.distinct()\
|
||||||
.distinct()
|
|
||||||
.order_by(Lower('faction_name'))
|
.order_by(Lower('faction_name'))
|
||||||
)
|
|
||||||
return tuple(
|
return tuple(
|
||||||
(x['faction_id'], x['faction_name']) for x in qs
|
(x['faction_id'], x['faction_name']) for x in qs
|
||||||
)
|
)
|
||||||
@@ -262,14 +266,15 @@ class MainFactionFilter(admin.SimpleListFilter):
|
|||||||
def queryset(self, request, qs):
|
def queryset(self, request, qs):
|
||||||
if self.value() is None:
|
if self.value() is None:
|
||||||
return qs.all()
|
return qs.all()
|
||||||
if qs.model == User:
|
else:
|
||||||
return qs.filter(profile__main_character__faction_id=self.value())
|
if qs.model == User:
|
||||||
return qs.filter(
|
return qs.filter(profile__main_character__faction_id=self.value())
|
||||||
user__profile__main_character__faction_id=self.value()
|
else:
|
||||||
)
|
return qs.filter(
|
||||||
|
user__profile__main_character__faction_id=self.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.display(description="Update main character model from ESI")
|
|
||||||
def update_main_character_model(modeladmin, request, queryset):
|
def update_main_character_model(modeladmin, request, queryset):
|
||||||
tasks_count = 0
|
tasks_count = 0
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
@@ -278,48 +283,21 @@ def update_main_character_model(modeladmin, request, queryset):
|
|||||||
tasks_count += 1
|
tasks_count += 1
|
||||||
|
|
||||||
modeladmin.message_user(
|
modeladmin.message_user(
|
||||||
request, f'Update from ESI started for {tasks_count} characters'
|
request,
|
||||||
|
f'Update from ESI started for {tasks_count} characters'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
update_main_character_model.short_description = \
|
||||||
|
'Update main character model from ESI'
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(BaseUserAdmin):
|
||||||
"""Extending Django's UserAdmin model
|
"""Extending Django's UserAdmin model
|
||||||
|
|
||||||
Behavior of groups and characters columns can be configured via settings
|
Behavior of groups and characters columns can be configured via settings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
|
||||||
ordering = ('username', )
|
|
||||||
list_select_related = ('profile__state', 'profile__main_character')
|
|
||||||
show_full_result_count = True
|
|
||||||
list_display = (
|
|
||||||
user_profile_pic,
|
|
||||||
user_username,
|
|
||||||
'_state',
|
|
||||||
'_groups',
|
|
||||||
user_main_organization,
|
|
||||||
'_characters',
|
|
||||||
'is_active',
|
|
||||||
'date_joined',
|
|
||||||
'_role'
|
|
||||||
)
|
|
||||||
list_display_links = None
|
|
||||||
list_filter = (
|
|
||||||
'profile__state',
|
|
||||||
'groups',
|
|
||||||
MainCorporationsFilter,
|
|
||||||
MainAllianceFilter,
|
|
||||||
MainFactionFilter,
|
|
||||||
'is_active',
|
|
||||||
'date_joined',
|
|
||||||
'is_staff',
|
|
||||||
'is_superuser'
|
|
||||||
)
|
|
||||||
search_fields = ('username', 'character_ownerships__character__character_name')
|
|
||||||
readonly_fields = ('date_joined', 'last_login')
|
|
||||||
filter_horizontal = ('groups', 'user_permissions',)
|
|
||||||
form = UserChangeForm
|
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
"all": ("authentication/css/admin.css",)
|
"all": ("authentication/css/admin.css",)
|
||||||
@@ -329,21 +307,9 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.prefetch_related("character_ownerships__character", "groups")
|
return qs.prefetch_related("character_ownerships__character", "groups")
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
|
||||||
"""Inject current request into change form object."""
|
|
||||||
|
|
||||||
MyForm = super().get_form(request, obj, **kwargs)
|
|
||||||
if obj:
|
|
||||||
class MyFormInjected(MyForm):
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
kwargs['request'] = request
|
|
||||||
return MyForm(*args, **kwargs)
|
|
||||||
|
|
||||||
return MyFormInjected
|
|
||||||
return MyForm
|
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
actions = super().get_actions(request)
|
actions = super(BaseUserAdmin, self).get_actions(request)
|
||||||
|
|
||||||
actions[update_main_character_model.__name__] = (
|
actions[update_main_character_model.__name__] = (
|
||||||
update_main_character_model,
|
update_main_character_model,
|
||||||
update_main_character_model.__name__,
|
update_main_character_model.__name__,
|
||||||
@@ -387,6 +353,39 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
||||||
|
ordering = ('username', )
|
||||||
|
list_select_related = ('profile__state', 'profile__main_character')
|
||||||
|
show_full_result_count = True
|
||||||
|
list_display = (
|
||||||
|
user_profile_pic,
|
||||||
|
user_username,
|
||||||
|
'_state',
|
||||||
|
'_groups',
|
||||||
|
user_main_organization,
|
||||||
|
'_characters',
|
||||||
|
'is_active',
|
||||||
|
'date_joined',
|
||||||
|
'_role'
|
||||||
|
)
|
||||||
|
list_display_links = None
|
||||||
|
list_filter = (
|
||||||
|
'profile__state',
|
||||||
|
'groups',
|
||||||
|
MainCorporationsFilter,
|
||||||
|
MainAllianceFilter,
|
||||||
|
MainFactionFilter,
|
||||||
|
'is_active',
|
||||||
|
'date_joined',
|
||||||
|
'is_staff',
|
||||||
|
'is_superuser'
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
'username',
|
||||||
|
'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())
|
||||||
characters = [obj.character.character_name for obj in character_ownerships]
|
characters = [obj.character.character_name for obj in character_ownerships]
|
||||||
@@ -395,16 +394,22 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
|
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
|
||||||
)
|
)
|
||||||
|
|
||||||
@admin.display(ordering="profile__state")
|
_characters.short_description = 'characters'
|
||||||
|
|
||||||
def _state(self, obj):
|
def _state(self, obj):
|
||||||
return obj.profile.state.name
|
return obj.profile.state.name
|
||||||
|
|
||||||
|
_state.short_description = 'state'
|
||||||
|
_state.admin_order_field = 'profile__state'
|
||||||
|
|
||||||
def _groups(self, obj):
|
def _groups(self, obj):
|
||||||
my_groups = sorted(group.name for group in list(obj.groups.all()))
|
my_groups = sorted(group.name for group in list(obj.groups.all()))
|
||||||
return self._list_2_html_w_tooltips(
|
return self._list_2_html_w_tooltips(
|
||||||
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_groups.short_description = 'groups'
|
||||||
|
|
||||||
def _role(self, obj):
|
def _role(self, obj):
|
||||||
if obj.is_superuser:
|
if obj.is_superuser:
|
||||||
role = 'Superuser'
|
role = 'Superuser'
|
||||||
@@ -414,6 +419,8 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
role = 'User'
|
role = 'User'
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
_role.short_description = 'role'
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
return request.user.has_perm('auth.change_user')
|
return request.user.has_perm('auth.change_user')
|
||||||
|
|
||||||
@@ -435,16 +442,9 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
if obj_state:
|
if obj_state:
|
||||||
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
|
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
|
||||||
groups_qs = groups_qs | matching_groups_qs
|
groups_qs = groups_qs | matching_groups_qs
|
||||||
kwargs["queryset"] = groups_qs.order_by(Lower("name"))
|
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)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
if obj and not request.user.is_superuser:
|
|
||||||
return self.readonly_fields + (
|
|
||||||
"is_staff", "is_superuser", "user_permissions"
|
|
||||||
)
|
|
||||||
return self.readonly_fields
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(State)
|
@admin.register(State)
|
||||||
class StateAdmin(admin.ModelAdmin):
|
class StateAdmin(admin.ModelAdmin):
|
||||||
@@ -455,9 +455,10 @@ class StateAdmin(admin.ModelAdmin):
|
|||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
return qs.annotate(user_count=Count("userprofile__id"))
|
return qs.annotate(user_count=Count("userprofile__id"))
|
||||||
|
|
||||||
@admin.display(description="Users", ordering="user_count")
|
|
||||||
def _user_count(self, obj):
|
def _user_count(self, obj):
|
||||||
return obj.user_count
|
return obj.user_count
|
||||||
|
_user_count.short_description = 'Users'
|
||||||
|
_user_count.admin_order_field = 'user_count'
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
@@ -513,13 +514,13 @@ class StateAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
return super().get_fieldsets(request, obj=obj)
|
return super().get_fieldsets(request, obj=obj)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
return self.readonly_fields + ("permissions",)
|
|
||||||
return self.readonly_fields
|
|
||||||
|
|
||||||
|
|
||||||
class BaseOwnershipAdmin(admin.ModelAdmin):
|
class BaseOwnershipAdmin(admin.ModelAdmin):
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": ("authentication/css/admin.css",)
|
||||||
|
}
|
||||||
|
|
||||||
list_select_related = (
|
list_select_related = (
|
||||||
'user__profile__state', 'user__profile__main_character', 'character')
|
'user__profile__state', 'user__profile__main_character', 'character')
|
||||||
list_display = (
|
list_display = (
|
||||||
@@ -540,11 +541,6 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
|
|||||||
MainAllianceFilter,
|
MainAllianceFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Media:
|
|
||||||
css = {
|
|
||||||
"all": ("authentication/css/admin.css",)
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj and obj.pk:
|
if obj and obj.pk:
|
||||||
return 'owner_hash', 'character'
|
return 'owner_hash', 'character'
|
||||||
|
|||||||
@@ -1,66 +1,8 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth.authentication.models import User
|
from allianceauth.authentication.models import User
|
||||||
|
|
||||||
|
|
||||||
class RegistrationForm(forms.Form):
|
class RegistrationForm(forms.Form):
|
||||||
email = forms.EmailField(label=_('Email'), max_length=254, required=True)
|
email = forms.EmailField(label=_('Email'), max_length=254, required=True)
|
||||||
|
|
||||||
class _meta:
|
class _meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
|
|
||||||
class UserProfileForm(ModelForm):
|
|
||||||
"""Allows specifying FK querysets through kwarg"""
|
|
||||||
|
|
||||||
def __init__(self, querysets=None, *args, **kwargs):
|
|
||||||
querysets = querysets or {}
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for field, qs in querysets.items():
|
|
||||||
self.fields[field].queryset = qs
|
|
||||||
|
|
||||||
|
|
||||||
class UserChangeForm(BaseUserChangeForm):
|
|
||||||
"""Add custom cleaning to UserChangeForm"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.request = kwargs.pop("request") # Inject current request into form object
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
if not self.request.user.is_superuser:
|
|
||||||
if self.instance:
|
|
||||||
current_restricted = set(
|
|
||||||
self.instance.groups.filter(
|
|
||||||
authgroup__restricted=True
|
|
||||||
).values_list("pk", flat=True)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
current_restricted = set()
|
|
||||||
new_restricted = set(
|
|
||||||
cleaned_data["groups"].filter(
|
|
||||||
authgroup__restricted=True
|
|
||||||
).values_list("pk", flat=True)
|
|
||||||
)
|
|
||||||
if current_restricted != new_restricted:
|
|
||||||
restricted_removed = current_restricted - new_restricted
|
|
||||||
restricted_added = new_restricted - current_restricted
|
|
||||||
restricted_changed = restricted_removed | restricted_added
|
|
||||||
restricted_names_qs = Group.objects.filter(
|
|
||||||
pk__in=restricted_changed
|
|
||||||
).values_list("name", flat=True)
|
|
||||||
restricted_names = ",".join(list(restricted_names_qs))
|
|
||||||
raise ValidationError(
|
|
||||||
{
|
|
||||||
"groups": _(
|
|
||||||
"You are not allowed to add or remove these "
|
|
||||||
"restricted groups: %s" % restricted_names
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
from django.conf.urls import url, include
|
from django.conf.urls import include
|
||||||
|
|
||||||
from allianceauth.authentication import views
|
from allianceauth.authentication import views
|
||||||
|
from django.urls import re_path
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^activate/complete/$', views.activation_complete, name='registration_activation_complete'),
|
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),
|
||||||
# The activation key can make use of any character from the
|
# The activation key can make use of any character from the
|
||||||
# URL-safe base64 alphabet, plus the colon as a separator.
|
# URL-safe base64 alphabet, plus the colon as a separator.
|
||||||
url(r'^activate/(?P<activation_key>[-:\w]+)/$', views.ActivationView.as_view(), name='registration_activate'),
|
re_path(r'^activate/(?P<activation_key>[-:\w]+)/$', views.ActivationView.as_view(), name='registration_activate'),
|
||||||
url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'),
|
path('register/', views.RegistrationView.as_view(), name='registration_register'),
|
||||||
url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
|
path('register/complete/', views.registration_complete, name='registration_complete'),
|
||||||
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
|
path('register/closed/', views.registration_closed, name='registration_disallowed'),
|
||||||
url(r'', include('django.contrib.auth.urls')),
|
path('', include('django.contrib.auth.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
45
allianceauth/authentication/middleware.py
Normal file
45
allianceauth/authentication/middleware.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsMiddleware(MiddlewareMixin):
|
||||||
|
def process_response(self, request, response):
|
||||||
|
"""Django Middleware: User Settings."""
|
||||||
|
|
||||||
|
# Intercept the built in django /setlang/ view and also save it to Database.
|
||||||
|
# Note the annoymous user check, only logged in users will ever hit the DB here
|
||||||
|
if request.path == '/i18n/setlang/' and not request.user.is_anonymous:
|
||||||
|
try:
|
||||||
|
request.user.profile.language = request.POST['language']
|
||||||
|
request.user.profile.save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
|
||||||
|
# Only act during the login flow, _after_ user is activated (step 2: post-sso)
|
||||||
|
elif request.path == '/sso/login' and not request.user.is_anonymous:
|
||||||
|
# Set the Language Cookie, if it doesnt match the DB
|
||||||
|
# Null = hasnt been set by the user ever, dont act.
|
||||||
|
try:
|
||||||
|
if request.user.profile.language != request.LANGUAGE_CODE and request.user.profile.language is not None:
|
||||||
|
response.set_cookie(key=settings.LANGUAGE_COOKIE_NAME,
|
||||||
|
value=request.user.profile.language,
|
||||||
|
max_age=settings.LANGUAGE_COOKIE_AGE)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
|
||||||
|
# Set our Night mode flag from the DB
|
||||||
|
# Null = hasnt been set by the user ever, dont act.
|
||||||
|
#
|
||||||
|
# Night mode intercept is not needed in this middleware.
|
||||||
|
# is saved direct to DB in NightModeRedirectView
|
||||||
|
try:
|
||||||
|
if request.user.profile.night_mode is not None:
|
||||||
|
request.session["NIGHT_MODE"] = request.user.profile.night_mode
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.0.2 on 2022-02-26 03:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0019_merge_20211026_0919'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(blank=True, choices=[('en', 'English'), ('de', 'German'), ('es', 'Spanish'), ('zh-hans', 'Chinese Simplified'), ('ru', 'Russian'), ('ko', 'Korean'), ('fr', 'French'), ('ja', 'Japanese'), ('it', 'Italian')], default='', max_length=10, verbose_name='Language'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='night_mode',
|
||||||
|
field=models.BooleanField(blank=True, null=True, verbose_name='Night Mode'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,9 +2,10 @@ import logging
|
|||||||
|
|
||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
|
||||||
from allianceauth.notifications import notify
|
from allianceauth.notifications import notify
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from .managers import CharacterOwnershipManager, StateManager
|
from .managers import CharacterOwnershipManager, StateManager
|
||||||
|
|
||||||
@@ -62,9 +63,39 @@ class UserProfile(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ('change',)
|
default_permissions = ('change',)
|
||||||
|
|
||||||
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
|
user = models.OneToOneField(
|
||||||
main_character = models.OneToOneField(EveCharacter, blank=True, null=True, on_delete=models.SET_NULL)
|
User,
|
||||||
state = models.ForeignKey(State, on_delete=models.SET_DEFAULT, default=get_guest_state_pk)
|
related_name='profile',
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
main_character = models.OneToOneField(
|
||||||
|
EveCharacter,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL)
|
||||||
|
state = models.ForeignKey(
|
||||||
|
State,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
default=get_guest_state_pk)
|
||||||
|
LANGUAGE_CHOICES = [
|
||||||
|
('en', _('English')),
|
||||||
|
('de', _('German')),
|
||||||
|
('es', _('Spanish')),
|
||||||
|
('zh-hans', _('Chinese Simplified')),
|
||||||
|
('ru', _('Russian')),
|
||||||
|
('ko', _('Korean')),
|
||||||
|
('fr', _('French')),
|
||||||
|
('ja', _('Japanese')),
|
||||||
|
('it', _('Italian')),
|
||||||
|
]
|
||||||
|
language = models.CharField(
|
||||||
|
_("Language"), max_length=10,
|
||||||
|
choices=LANGUAGE_CHOICES,
|
||||||
|
blank=True,
|
||||||
|
default='')
|
||||||
|
night_mode = models.BooleanField(
|
||||||
|
_("Night Mode"),
|
||||||
|
blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
def assign_state(self, state=None, commit=True):
|
def assign_state(self, state=None, commit=True):
|
||||||
if not state:
|
if not state:
|
||||||
@@ -93,8 +124,6 @@ class UserProfile(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
|
|
||||||
class CharacterOwnership(models.Model):
|
class CharacterOwnership(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ('change', 'delete')
|
default_permissions = ('change', 'delete')
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
|
from .models import (
|
||||||
|
CharacterOwnership,
|
||||||
|
UserProfile,
|
||||||
|
get_guest_state,
|
||||||
|
State,
|
||||||
|
OwnershipRecord)
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
||||||
@@ -11,7 +16,7 @@ from allianceauth.eveonline.models import EveCharacter
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
state_changed = Signal(providing_args=['user', 'state'])
|
state_changed = Signal()
|
||||||
|
|
||||||
|
|
||||||
def trigger_state_check(state):
|
def trigger_state_check(state):
|
||||||
@@ -71,7 +76,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
|
|||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
def create_required_models(sender, instance, created, *args, **kwargs):
|
def create_required_models(sender, instance, created, *args, **kwargs):
|
||||||
# ensure all users have a model
|
# ensure all users have our Sub-Models
|
||||||
if created:
|
if created:
|
||||||
logger.debug(f'User {instance} created. Creating default UserProfile.')
|
logger.debug(f'User {instance} created. Creating default UserProfile.')
|
||||||
UserProfile.objects.get_or_create(user=instance)
|
UserProfile.objects.get_or_create(user=instance)
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
@@ -1,69 +1,74 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import logging
|
from collections import namedtuple
|
||||||
from typing import List, Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from redis import Redis
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
from redis import Redis, RedisError
|
|
||||||
|
|
||||||
from allianceauth.utils.cache import get_redis_client
|
from django_redis import get_redis_connection
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
_TaskCounts = namedtuple(
|
||||||
|
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _RedisStub:
|
def dashboard_results(hours: int) -> _TaskCounts:
|
||||||
"""Stub of a Redis client.
|
"""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 []
|
||||||
|
|
||||||
It's purpose is to prevent EventSeries objects from trying to access Redis
|
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||||
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
earliest_events = list()
|
||||||
"""
|
succeeded = SucceededTaskSeries()
|
||||||
|
succeeded_count = succeeded.count(earliest=earliest)
|
||||||
def delete(self, *args, **kwargs):
|
earliest_events += earliest_if_exists(succeeded, earliest)
|
||||||
pass
|
retried = RetriedTaskSeries()
|
||||||
|
retried_count = retried.count(earliest=earliest)
|
||||||
def incr(self, *args, **kwargs):
|
earliest_events += earliest_if_exists(retried, earliest)
|
||||||
return 0
|
failed = FailedTaskSeries()
|
||||||
|
failed_count = failed.count(earliest=earliest)
|
||||||
def zadd(self, *args, **kwargs):
|
earliest_events += earliest_if_exists(failed, earliest)
|
||||||
pass
|
return _TaskCounts(
|
||||||
|
succeeded=succeeded_count,
|
||||||
def zcount(self, *args, **kwargs):
|
retried=retried_count,
|
||||||
pass
|
failed=failed_count,
|
||||||
|
total=succeeded_count + retried_count + failed_count,
|
||||||
def zrangebyscore(self, *args, **kwargs):
|
earliest_task=min(earliest_events) if earliest_events else None,
|
||||||
pass
|
hours=hours,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventSeries:
|
class EventSeries:
|
||||||
"""API for recording and analyzing a series of events."""
|
"""Base class for recording and analysing a series of events.
|
||||||
|
|
||||||
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
This class must be inherited from and the child class must define KEY_ID.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
_ROOT_KEY = "ALLIANCEAUTH_TASK_SERIES"
|
||||||
self._redis = get_redis_client() if not redis else redis
|
|
||||||
try:
|
def __init__(
|
||||||
if not self._redis.ping():
|
self,
|
||||||
raise RuntimeError()
|
redis: Redis = None,
|
||||||
except (AttributeError, RedisError, RuntimeError):
|
) -> None:
|
||||||
logger.exception(
|
if type(self) == EventSeries:
|
||||||
"Failed to establish a connection with Redis. "
|
raise TypeError("Can not instantiate base class.")
|
||||||
"This EventSeries object is disabled.",
|
if not hasattr(self, "KEY_ID"):
|
||||||
|
raise ValueError("KEY_ID not defined")
|
||||||
|
self._redis = get_redis_connection("default") 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._redis = _RedisStub()
|
|
||||||
self._key_id = str(key_id)
|
|
||||||
self.clear()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_disabled(self):
|
|
||||||
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
|
||||||
return isinstance(self._redis, _RedisStub)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _key_counter(self):
|
def _key_counter(self):
|
||||||
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
return f"{self._ROOT_KEY}_{self.KEY_ID}_COUNTER"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _key_sorted_set(self):
|
def _key_sorted_set(self):
|
||||||
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
|
return f"{self._ROOT_KEY}_{self.KEY_ID}_SORTED_SET"
|
||||||
|
|
||||||
def add(self, event_time: dt.datetime = None) -> None:
|
def add(self, event_time: dt.datetime = None) -> None:
|
||||||
"""Add event.
|
"""Add event.
|
||||||
@@ -128,3 +133,21 @@ class EventSeries:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _cast_scores_to_dt(score) -> dt.datetime:
|
def _cast_scores_to_dt(score) -> dt.datetime:
|
||||||
return dt.datetime.fromtimestamp(float(score), tz=utc)
|
return dt.datetime.fromtimestamp(float(score), tz=utc)
|
||||||
|
|
||||||
|
|
||||||
|
class SucceededTaskSeries(EventSeries):
|
||||||
|
"""A task has succeeded."""
|
||||||
|
|
||||||
|
KEY_ID = "SUCCEEDED"
|
||||||
|
|
||||||
|
|
||||||
|
class RetriedTaskSeries(EventSeries):
|
||||||
|
"""A task has been retried."""
|
||||||
|
|
||||||
|
KEY_ID = "RETRIED"
|
||||||
|
|
||||||
|
|
||||||
|
class FailedTaskSeries(EventSeries):
|
||||||
|
"""A task has failed."""
|
||||||
|
|
||||||
|
KEY_ID = "FAILED"
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
from celery.signals import (
|
from celery.signals import task_failure, task_retry, task_success, worker_ready
|
||||||
task_failure,
|
|
||||||
task_internal_error,
|
|
||||||
task_retry,
|
|
||||||
task_success,
|
|
||||||
worker_ready
|
|
||||||
)
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .counters import failed_tasks, retried_tasks, succeeded_tasks
|
from .event_series import FailedTaskSeries, RetriedTaskSeries, SucceededTaskSeries
|
||||||
|
|
||||||
|
|
||||||
def reset_counters():
|
def reset_counters():
|
||||||
"""Reset all counters for the celery status."""
|
"""Reset all counters for the celery status."""
|
||||||
succeeded_tasks.clear()
|
SucceededTaskSeries().clear()
|
||||||
failed_tasks.clear()
|
FailedTaskSeries().clear()
|
||||||
retried_tasks.clear()
|
RetriedTaskSeries().clear()
|
||||||
|
|
||||||
|
|
||||||
def is_enabled() -> bool:
|
def is_enabled() -> bool:
|
||||||
@@ -33,22 +27,16 @@ def reset_counters_when_celery_restarted(*args, **kwargs):
|
|||||||
@task_success.connect
|
@task_success.connect
|
||||||
def record_task_succeeded(*args, **kwargs):
|
def record_task_succeeded(*args, **kwargs):
|
||||||
if is_enabled():
|
if is_enabled():
|
||||||
succeeded_tasks.add()
|
SucceededTaskSeries().add()
|
||||||
|
|
||||||
|
|
||||||
@task_retry.connect
|
@task_retry.connect
|
||||||
def record_task_retried(*args, **kwargs):
|
def record_task_retried(*args, **kwargs):
|
||||||
if is_enabled():
|
if is_enabled():
|
||||||
retried_tasks.add()
|
RetriedTaskSeries().add()
|
||||||
|
|
||||||
|
|
||||||
@task_failure.connect
|
@task_failure.connect
|
||||||
def record_task_failed(*args, **kwargs):
|
def record_task_failed(*args, **kwargs):
|
||||||
if is_enabled():
|
if is_enabled():
|
||||||
failed_tasks.add()
|
FailedTaskSeries().add()
|
||||||
|
|
||||||
|
|
||||||
@task_internal_error.connect
|
|
||||||
def record_task_internal_error(*args, **kwargs):
|
|
||||||
if is_enabled():
|
|
||||||
failed_tasks.add()
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,51 +1,49 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
from redis import RedisError
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from allianceauth.authentication.task_statistics.event_series import (
|
from allianceauth.authentication.task_statistics.event_series import (
|
||||||
EventSeries,
|
EventSeries,
|
||||||
_RedisStub,
|
FailedTaskSeries,
|
||||||
|
RetriedTaskSeries,
|
||||||
|
SucceededTaskSeries,
|
||||||
|
dashboard_results,
|
||||||
)
|
)
|
||||||
|
|
||||||
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
|
||||||
|
|
||||||
|
|
||||||
class TestEventSeries(TestCase):
|
class TestEventSeries(TestCase):
|
||||||
def test_should_abort_without_redis_client(self):
|
"""Testing EventSeries class."""
|
||||||
# when
|
|
||||||
with patch(MODULE_PATH + ".get_redis_client") as mock:
|
|
||||||
mock.return_value = None
|
|
||||||
events = EventSeries("dummy")
|
|
||||||
# then
|
|
||||||
self.assertTrue(events._redis, _RedisStub)
|
|
||||||
self.assertTrue(events.is_disabled)
|
|
||||||
|
|
||||||
def test_should_disable_itself_if_redis_not_available_1(self):
|
class IncompleteEvents(EventSeries):
|
||||||
# when
|
"""Child class without KEY ID"""
|
||||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
|
||||||
mock_get_master_client.return_value.ping.side_effect = RedisError
|
|
||||||
events = EventSeries("dummy")
|
|
||||||
# then
|
|
||||||
self.assertIsInstance(events._redis, _RedisStub)
|
|
||||||
self.assertTrue(events.is_disabled)
|
|
||||||
|
|
||||||
def test_should_disable_itself_if_redis_not_available_2(self):
|
class MyEventSeries(EventSeries):
|
||||||
|
KEY_ID = "TEST"
|
||||||
|
|
||||||
|
def test_should_create_object(self):
|
||||||
# when
|
# when
|
||||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
events = self.MyEventSeries()
|
||||||
mock_get_master_client.return_value.ping.return_value = False
|
|
||||||
events = EventSeries("dummy")
|
|
||||||
# then
|
# then
|
||||||
self.assertIsInstance(events._redis, _RedisStub)
|
self.assertIsInstance(events, self.MyEventSeries)
|
||||||
self.assertTrue(events.is_disabled)
|
|
||||||
|
def test_should_abort_when_redis_client_invalid(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.MyEventSeries(redis="invalid")
|
||||||
|
|
||||||
|
def test_should_not_allow_instantiation_of_base_class(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
EventSeries()
|
||||||
|
|
||||||
|
def test_should_not_allow_creating_child_class_without_key_id(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.IncompleteEvents()
|
||||||
|
|
||||||
def test_should_add_event(self):
|
def test_should_add_event(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
# when
|
# when
|
||||||
events.add()
|
events.add()
|
||||||
# then
|
# then
|
||||||
@@ -55,7 +53,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_add_event_with_specified_time(self):
|
def test_should_add_event_with_specified_time(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
|
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
|
||||||
# when
|
# when
|
||||||
events.add(my_time)
|
events.add(my_time)
|
||||||
@@ -66,7 +65,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_count_events(self):
|
def test_should_count_events(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add()
|
events.add()
|
||||||
events.add()
|
events.add()
|
||||||
# when
|
# when
|
||||||
@@ -76,7 +76,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_count_zero(self):
|
def test_should_count_zero(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
# when
|
# when
|
||||||
result = events.count()
|
result = events.count()
|
||||||
# then
|
# then
|
||||||
@@ -84,7 +85,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_count_events_within_timeframe_1(self):
|
def test_should_count_events_within_timeframe_1(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
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, 10, tzinfo=utc))
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
@@ -99,7 +101,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_count_events_within_timeframe_2(self):
|
def test_should_count_events_within_timeframe_2(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
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, 10, tzinfo=utc))
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
@@ -111,7 +114,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_count_events_within_timeframe_3(self):
|
def test_should_count_events_within_timeframe_3(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
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, 10, tzinfo=utc))
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
@@ -123,7 +127,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_clear_events(self):
|
def test_should_clear_events(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add()
|
events.add()
|
||||||
events.add()
|
events.add()
|
||||||
# when
|
# when
|
||||||
@@ -133,7 +138,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_return_date_of_first_event(self):
|
def test_should_return_date_of_first_event(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
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, 10, tzinfo=utc))
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
@@ -145,7 +151,8 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_return_date_of_first_event_with_range(self):
|
def test_should_return_date_of_first_event_with_range(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
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, 10, tzinfo=utc))
|
||||||
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
@@ -159,10 +166,57 @@ class TestEventSeries(TestCase):
|
|||||||
|
|
||||||
def test_should_return_all_events(self):
|
def test_should_return_all_events(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = self.MyEventSeries()
|
||||||
|
events.clear()
|
||||||
events.add()
|
events.add()
|
||||||
events.add()
|
events.add()
|
||||||
# when
|
# when
|
||||||
results = events.all()
|
results = events.all()
|
||||||
# then
|
# then
|
||||||
self.assertEqual(len(results), 2)
|
self.assertEqual(len(results), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDashboardResults(TestCase):
|
||||||
|
def test_should_return_counts_for_given_timeframe_only(self):
|
||||||
|
# given
|
||||||
|
earliest_task = now() - dt.timedelta(minutes=15)
|
||||||
|
succeeded = SucceededTaskSeries()
|
||||||
|
succeeded.clear()
|
||||||
|
succeeded.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
|
succeeded.add(earliest_task)
|
||||||
|
succeeded.add()
|
||||||
|
succeeded.add()
|
||||||
|
retried = RetriedTaskSeries()
|
||||||
|
retried.clear()
|
||||||
|
retried.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
|
retried.add(now() - dt.timedelta(seconds=30))
|
||||||
|
retried.add()
|
||||||
|
failed = FailedTaskSeries()
|
||||||
|
failed.clear()
|
||||||
|
failed.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
|
failed.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 = SucceededTaskSeries()
|
||||||
|
succeeded.clear()
|
||||||
|
retried = RetriedTaskSeries()
|
||||||
|
retried.clear()
|
||||||
|
failed = FailedTaskSeries()
|
||||||
|
failed.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)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from celery.exceptions import Retry
|
|||||||
|
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from allianceauth.authentication.task_statistics.counters import (
|
from allianceauth.authentication.task_statistics.event_series import (
|
||||||
failed_tasks,
|
FailedTaskSeries,
|
||||||
retried_tasks,
|
RetriedTaskSeries,
|
||||||
succeeded_tasks,
|
SucceededTaskSeries,
|
||||||
)
|
)
|
||||||
from allianceauth.authentication.task_statistics.signals import (
|
from allianceauth.authentication.task_statistics.signals import (
|
||||||
reset_counters,
|
reset_counters,
|
||||||
@@ -17,16 +17,15 @@ from allianceauth.eveonline.tasks import update_character
|
|||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
CELERY_ALWAYS_EAGER=True, ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||||
)
|
)
|
||||||
class TestTaskSignals(TestCase):
|
class TestTaskSignals(TestCase):
|
||||||
fixtures = ["disable_analytics"]
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
def test_should_record_successful_task(self):
|
def test_should_record_successful_task(self):
|
||||||
# given
|
# given
|
||||||
succeeded_tasks.clear()
|
events = SucceededTaskSeries()
|
||||||
retried_tasks.clear()
|
events.clear()
|
||||||
failed_tasks.clear()
|
|
||||||
# when
|
# when
|
||||||
with patch(
|
with patch(
|
||||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
@@ -34,15 +33,12 @@ class TestTaskSignals(TestCase):
|
|||||||
mock_update.return_value = None
|
mock_update.return_value = None
|
||||||
update_character.delay(1)
|
update_character.delay(1)
|
||||||
# then
|
# then
|
||||||
self.assertEqual(succeeded_tasks.count(), 1)
|
self.assertEqual(events.count(), 1)
|
||||||
self.assertEqual(retried_tasks.count(), 0)
|
|
||||||
self.assertEqual(failed_tasks.count(), 0)
|
|
||||||
|
|
||||||
def test_should_record_retried_task(self):
|
def test_should_record_retried_task(self):
|
||||||
# given
|
# given
|
||||||
succeeded_tasks.clear()
|
events = RetriedTaskSeries()
|
||||||
retried_tasks.clear()
|
events.clear()
|
||||||
failed_tasks.clear()
|
|
||||||
# when
|
# when
|
||||||
with patch(
|
with patch(
|
||||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
@@ -50,15 +46,12 @@ class TestTaskSignals(TestCase):
|
|||||||
mock_update.side_effect = Retry
|
mock_update.side_effect = Retry
|
||||||
update_character.delay(1)
|
update_character.delay(1)
|
||||||
# then
|
# then
|
||||||
self.assertEqual(succeeded_tasks.count(), 0)
|
self.assertEqual(events.count(), 1)
|
||||||
self.assertEqual(failed_tasks.count(), 0)
|
|
||||||
self.assertEqual(retried_tasks.count(), 1)
|
|
||||||
|
|
||||||
def test_should_record_failed_task(self):
|
def test_should_record_failed_task(self):
|
||||||
# given
|
# given
|
||||||
succeeded_tasks.clear()
|
events = FailedTaskSeries()
|
||||||
retried_tasks.clear()
|
events.clear()
|
||||||
failed_tasks.clear()
|
|
||||||
# when
|
# when
|
||||||
with patch(
|
with patch(
|
||||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
@@ -66,21 +59,28 @@ class TestTaskSignals(TestCase):
|
|||||||
mock_update.side_effect = RuntimeError
|
mock_update.side_effect = RuntimeError
|
||||||
update_character.delay(1)
|
update_character.delay(1)
|
||||||
# then
|
# then
|
||||||
self.assertEqual(succeeded_tasks.count(), 0)
|
self.assertEqual(events.count(), 1)
|
||||||
self.assertEqual(retried_tasks.count(), 0)
|
|
||||||
self.assertEqual(failed_tasks.count(), 1)
|
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
|
||||||
|
class TestResetCounters(TestCase):
|
||||||
def test_should_reset_counters(self):
|
def test_should_reset_counters(self):
|
||||||
# given
|
# given
|
||||||
succeeded_tasks.add()
|
succeeded = SucceededTaskSeries()
|
||||||
retried_tasks.add()
|
succeeded.clear()
|
||||||
failed_tasks.add()
|
succeeded.add()
|
||||||
|
retried = RetriedTaskSeries()
|
||||||
|
retried.clear()
|
||||||
|
retried.add()
|
||||||
|
failed = FailedTaskSeries()
|
||||||
|
failed.clear()
|
||||||
|
failed.add()
|
||||||
# when
|
# when
|
||||||
reset_counters()
|
reset_counters()
|
||||||
# then
|
# then
|
||||||
self.assertEqual(succeeded_tasks.count(), 0)
|
self.assertEqual(succeeded.count(), 0)
|
||||||
self.assertEqual(retried_tasks.count(), 0)
|
self.assertEqual(retried.count(), 0)
|
||||||
self.assertEqual(failed_tasks.count(), 0)
|
self.assertEqual(failed.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
class TestIsEnabled(TestCase):
|
class TestIsEnabled(TestCase):
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<form class="form-signin" role="form" action="" method="POST">
|
<form class="form-signin" role="form" action="" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
|
||||||
<br/>
|
<br>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
|
from django.db.models.signals import (
|
||||||
|
m2m_changed,
|
||||||
|
post_save,
|
||||||
|
pre_delete,
|
||||||
|
pre_save
|
||||||
|
)
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.authentication'
|
||||||
|
|
||||||
|
|
||||||
|
def patch(target, *args, **kwargs):
|
||||||
|
return mock.patch(f'{MODULE_PATH}{target}', *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_admin_change_view_url(obj: object) -> str:
|
def get_admin_change_view_url(obj: object) -> str:
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ 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
|
||||||
|
|
||||||
from django_webtest import WebTest
|
|
||||||
|
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
@@ -278,10 +276,10 @@ class TestOwnershipRecordAdmin(TestCaseWithTestData):
|
|||||||
class TestStateAdmin(TestCaseWithTestData):
|
class TestStateAdmin(TestCaseWithTestData):
|
||||||
fixtures = ["disable_analytics"]
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls) -> None:
|
self.modeladmin = StateAdmin(
|
||||||
super().setUpClass()
|
model=User, admin_site=AdminSite()
|
||||||
cls.modeladmin = StateAdmin(model=User, admin_site=AdminSite())
|
)
|
||||||
|
|
||||||
def test_change_view_loads_normally(self):
|
def test_change_view_loads_normally(self):
|
||||||
User.objects.create_superuser(
|
User.objects.create_superuser(
|
||||||
@@ -545,74 +543,7 @@ class TestUserAdmin(TestCaseWithTestData):
|
|||||||
self.assertEqual(response.status_code, expected)
|
self.assertEqual(response.status_code, expected)
|
||||||
|
|
||||||
|
|
||||||
class TestStateAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
super().setUpClass()
|
|
||||||
cls.super_admin = User.objects.create_superuser("super_admin")
|
|
||||||
cls.staff_admin = User.objects.create_user("staff_admin")
|
|
||||||
cls.staff_admin.is_staff = True
|
|
||||||
cls.staff_admin.save()
|
|
||||||
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
|
|
||||||
[
|
|
||||||
"authentication.add_state",
|
|
||||||
"authentication.change_state",
|
|
||||||
"authentication.view_state",
|
|
||||||
],
|
|
||||||
cls.staff_admin
|
|
||||||
)
|
|
||||||
cls.superuser_exclusive_fields = ["permissions",]
|
|
||||||
|
|
||||||
def test_should_show_all_fields_to_superuser_for_add(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
page = self.app.get("/admin/authentication/state/add/")
|
|
||||||
# when
|
|
||||||
form = page.forms["state_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_not_show_all_fields_to_staff_admins_for_add(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
page = self.app.get("/admin/authentication/state/add/")
|
|
||||||
# when
|
|
||||||
form = page.forms["state_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertNotIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_show_all_fields_to_superuser_for_change(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
state = AuthUtils.get_member_state()
|
|
||||||
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
|
|
||||||
# when
|
|
||||||
form = page.forms["state_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
state = AuthUtils.get_member_state()
|
|
||||||
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
|
|
||||||
# when
|
|
||||||
form = page.forms["state_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertNotIn(field, form.fields)
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserAdminChangeForm(TestCase):
|
class TestUserAdminChangeForm(TestCase):
|
||||||
fixtures = ["disable_analytics"]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
@@ -621,7 +552,7 @@ class TestUserAdminChangeForm(TestCase):
|
|||||||
def test_should_show_groups_available_to_user_with_blue_state_only(self):
|
def test_should_show_groups_available_to_user_with_blue_state_only(self):
|
||||||
# given
|
# given
|
||||||
superuser = User.objects.create_superuser("Super")
|
superuser = User.objects.create_superuser("Super")
|
||||||
user = AuthUtils.create_user("bruce_wayne")
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
character = AuthUtils.add_main_character_2(
|
character = AuthUtils.add_main_character_2(
|
||||||
user,
|
user,
|
||||||
name="Bruce Wayne",
|
name="Bruce Wayne",
|
||||||
@@ -648,126 +579,6 @@ class TestUserAdminChangeForm(TestCase):
|
|||||||
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
|
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
|
||||||
|
|
||||||
|
|
||||||
class TestUserAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
|
||||||
fixtures = ["disable_analytics"]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
super().setUpClass()
|
|
||||||
cls.super_admin = User.objects.create_superuser("super_admin")
|
|
||||||
cls.staff_admin = User.objects.create_user("staff_admin")
|
|
||||||
cls.staff_admin.is_staff = True
|
|
||||||
cls.staff_admin.save()
|
|
||||||
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
|
|
||||||
[
|
|
||||||
"auth.change_user",
|
|
||||||
"auth.view_user",
|
|
||||||
"authentication.change_user",
|
|
||||||
"authentication.change_userprofile",
|
|
||||||
"authentication.view_user"
|
|
||||||
],
|
|
||||||
cls.staff_admin
|
|
||||||
)
|
|
||||||
cls.superuser_exclusive_fields = [
|
|
||||||
"is_staff", "is_superuser", "user_permissions"
|
|
||||||
]
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.user = AuthUtils.create_user("bruce_wayne")
|
|
||||||
|
|
||||||
def test_should_show_all_fields_to_superuser_for_change(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
|
|
||||||
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
|
||||||
# when
|
|
||||||
form = page.forms["user_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
|
||||||
# when
|
|
||||||
form = page.forms["user_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertNotIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_allow_super_admin_to_add_restricted_group_to_user(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
group_restricted = Group.objects.create(name="restricted group")
|
|
||||||
group_restricted.authgroup.restricted = True
|
|
||||||
group_restricted.authgroup.save()
|
|
||||||
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
|
||||||
form = page.forms["user_form"]
|
|
||||||
# when
|
|
||||||
form["groups"].select_multiple(texts=["restricted group"])
|
|
||||||
response = form.submit("save")
|
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.user.refresh_from_db()
|
|
||||||
self.assertIn(
|
|
||||||
"restricted group", self.user.groups.values_list("name", flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_should_not_allow_staff_admin_to_add_restricted_group_to_user(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
group_restricted = Group.objects.create(name="restricted group")
|
|
||||||
group_restricted.authgroup.restricted = True
|
|
||||||
group_restricted.authgroup.save()
|
|
||||||
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
|
||||||
form = page.forms["user_form"]
|
|
||||||
# when
|
|
||||||
form["groups"].select_multiple(texts=["restricted group"])
|
|
||||||
response = form.submit("save")
|
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertIn(
|
|
||||||
"You are not allowed to add or remove these restricted groups",
|
|
||||||
response.text
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_should_not_allow_staff_admin_to_remove_restricted_group_from_user(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
group_restricted = Group.objects.create(name="restricted group")
|
|
||||||
group_restricted.authgroup.restricted = True
|
|
||||||
group_restricted.authgroup.save()
|
|
||||||
self.user.groups.add(group_restricted)
|
|
||||||
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
|
||||||
form = page.forms["user_form"]
|
|
||||||
# when
|
|
||||||
form["groups"].select_multiple(texts=[])
|
|
||||||
response = form.submit("save")
|
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertIn(
|
|
||||||
"You are not allowed to add or remove these restricted groups",
|
|
||||||
response.text
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_should_allow_staff_admin_to_add_normal_group_to_user(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
Group.objects.create(name="normal group")
|
|
||||||
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
|
||||||
form = page.forms["user_form"]
|
|
||||||
# when
|
|
||||||
form["groups"].select_multiple(texts=["normal group"])
|
|
||||||
response = form.submit("save")
|
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.user.refresh_from_db()
|
|
||||||
self.assertIn("normal group", self.user.groups.values_list("name", flat=True))
|
|
||||||
|
|
||||||
|
|
||||||
class TestMakeServicesHooksActions(TestCaseWithTestData):
|
class TestMakeServicesHooksActions(TestCaseWithTestData):
|
||||||
|
|
||||||
class MyServicesHookTypeA(ServicesHook):
|
class MyServicesHookTypeA(ServicesHook):
|
||||||
|
|||||||
175
allianceauth/authentication/tests/test_middleware.py
Normal file
175
allianceauth/authentication/tests/test_middleware.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
from unittest import mock
|
||||||
|
from allianceauth.authentication.middleware import UserSettingsMiddleware
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserSettingsMiddlewareSaveLang(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.middleware = UserSettingsMiddleware(HttpResponse)
|
||||||
|
self.request = Mock()
|
||||||
|
self.request.headers = {
|
||||||
|
"User-Agent": "AUTOMATED TEST"
|
||||||
|
}
|
||||||
|
self.request.path = '/i18n/setlang/'
|
||||||
|
self.request.POST = {
|
||||||
|
'language': 'fr'
|
||||||
|
}
|
||||||
|
self.request.user.profile.language = 'de'
|
||||||
|
self.request.user.is_anonymous = False
|
||||||
|
self.response = Mock()
|
||||||
|
self.response.content = 'hello world'
|
||||||
|
|
||||||
|
def test_middleware_passthrough(self):
|
||||||
|
"""
|
||||||
|
Simply tests the middleware runs cleanly
|
||||||
|
"""
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.response, response)
|
||||||
|
|
||||||
|
def test_middleware_save_language_false_anonymous(self):
|
||||||
|
"""
|
||||||
|
Ensures the middleware wont change the usersettings
|
||||||
|
of a non-existent (anonymous) user
|
||||||
|
"""
|
||||||
|
self.request.user.is_anonymous = True
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.user.profile.language, 'de')
|
||||||
|
self.assertFalse(self.request.user.profile.save.called)
|
||||||
|
self.assertEqual(self.request.user.profile.save.call_count, 0)
|
||||||
|
|
||||||
|
def test_middleware_save_language_new(self):
|
||||||
|
"""
|
||||||
|
does the middleware change a language not set in the DB
|
||||||
|
"""
|
||||||
|
self.request.user.profile.language = None
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.user.profile.language, 'fr')
|
||||||
|
self.assertTrue(self.request.user.profile.save.called)
|
||||||
|
self.assertEqual(self.request.user.profile.save.call_count, 1)
|
||||||
|
|
||||||
|
def test_middleware_save_language_changed(self):
|
||||||
|
"""
|
||||||
|
Tests the middleware will change a language setting
|
||||||
|
"""
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.user.profile.language, 'fr')
|
||||||
|
self.assertTrue(self.request.user.profile.save.called)
|
||||||
|
self.assertEqual(self.request.user.profile.save.call_count, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserSettingsMiddlewareLoginFlow(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.middleware = UserSettingsMiddleware(HttpResponse)
|
||||||
|
self.request = Mock()
|
||||||
|
self.request.headers = {
|
||||||
|
"User-Agent": "AUTOMATED TEST"
|
||||||
|
}
|
||||||
|
self.request.path = '/sso/login'
|
||||||
|
self.request.session = {
|
||||||
|
'NIGHT_MODE': False
|
||||||
|
}
|
||||||
|
self.request.LANGUAGE_CODE = 'en'
|
||||||
|
self.request.user.profile.language = 'de'
|
||||||
|
self.request.user.profile.night_mode = True
|
||||||
|
self.request.user.is_anonymous = False
|
||||||
|
self.response = Mock()
|
||||||
|
self.response.content = 'hello world'
|
||||||
|
|
||||||
|
def test_middleware_passthrough(self):
|
||||||
|
"""
|
||||||
|
Simply tests the middleware runs cleanly
|
||||||
|
"""
|
||||||
|
middleware_response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.response, middleware_response)
|
||||||
|
|
||||||
|
def test_middleware_sets_language_cookie_true_no_cookie(self):
|
||||||
|
"""
|
||||||
|
tests the middleware will set a cookie, while none is set
|
||||||
|
"""
|
||||||
|
self.request.LANGUAGE_CODE = None
|
||||||
|
middleware_response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertTrue(middleware_response.set_cookie.called)
|
||||||
|
self.assertEqual(middleware_response.set_cookie.call_count, 1)
|
||||||
|
args, kwargs = middleware_response.set_cookie.call_args
|
||||||
|
self.assertEqual(kwargs['value'], 'de')
|
||||||
|
|
||||||
|
def test_middleware_sets_language_cookie_true_wrong_cookie(self):
|
||||||
|
"""
|
||||||
|
tests the middleware will set a cookie, while a different value is set
|
||||||
|
"""
|
||||||
|
middleware_response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertTrue(middleware_response.set_cookie.called)
|
||||||
|
self.assertEqual(middleware_response.set_cookie.call_count, 1)
|
||||||
|
args, kwargs = middleware_response.set_cookie.call_args
|
||||||
|
self.assertEqual(kwargs['value'], 'de')
|
||||||
|
|
||||||
|
def test_middleware_sets_language_cookie_false_anonymous(self):
|
||||||
|
"""
|
||||||
|
ensures the middleware wont set a value for a non existent user (anonymous)
|
||||||
|
"""
|
||||||
|
self.request.user.is_anonymous = True
|
||||||
|
middleware_response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertFalse = middleware_response.set_cookie.called
|
||||||
|
self.assertEqual(middleware_response.set_cookie.call_count, 0)
|
||||||
|
|
||||||
|
def test_middleware_sets_language_cookie_false_already_set(self):
|
||||||
|
"""
|
||||||
|
tests the middleware skips setting the cookie, if its already set correctly
|
||||||
|
"""
|
||||||
|
self.request.user.profile.language = 'en'
|
||||||
|
middleware_response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertFalse = middleware_response.set_cookie.called
|
||||||
|
self.assertEqual(middleware_response.set_cookie.call_count, 0)
|
||||||
|
|
||||||
|
def test_middleware_sets_night_mode_not_set(self):
|
||||||
|
"""
|
||||||
|
tests the middleware will set night_mode if not set
|
||||||
|
"""
|
||||||
|
self.request.session = {}
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
||||||
|
|
||||||
|
def test_middleware_sets_night_mode_set(self):
|
||||||
|
"""
|
||||||
|
tests the middleware will set night_mode if set.
|
||||||
|
"""
|
||||||
|
response = self.middleware.process_response(
|
||||||
|
self.request,
|
||||||
|
self.response
|
||||||
|
)
|
||||||
|
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
||||||
94
allianceauth/authentication/tests/test_signals.py
Normal file
94
allianceauth/authentication/tests/test_signals.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from allianceauth.authentication.models import User, UserProfile
|
||||||
|
from allianceauth.eveonline.models import (
|
||||||
|
EveCharacter,
|
||||||
|
EveCorporationInfo,
|
||||||
|
EveAllianceInfo
|
||||||
|
)
|
||||||
|
from django.db.models.signals import (
|
||||||
|
pre_save,
|
||||||
|
post_save,
|
||||||
|
pre_delete,
|
||||||
|
m2m_changed
|
||||||
|
)
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from . import patch
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserProfileSignals(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
state = AuthUtils.get_member_state()
|
||||||
|
|
||||||
|
self.char = EveCharacter.objects.create(
|
||||||
|
character_id='1234',
|
||||||
|
character_name='test character',
|
||||||
|
corporation_id='2345',
|
||||||
|
corporation_name='test corp',
|
||||||
|
corporation_ticker='tickr',
|
||||||
|
alliance_id='3456',
|
||||||
|
alliance_name='alliance name',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.alliance = EveAllianceInfo.objects.create(
|
||||||
|
alliance_id='3456',
|
||||||
|
alliance_name='alliance name',
|
||||||
|
alliance_ticker='TIKR',
|
||||||
|
executor_corp_id='2345',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.corp = EveCorporationInfo.objects.create(
|
||||||
|
corporation_id='2345',
|
||||||
|
corporation_name='corp name',
|
||||||
|
corporation_ticker='TIKK',
|
||||||
|
member_count=10,
|
||||||
|
alliance=self.alliance,
|
||||||
|
)
|
||||||
|
|
||||||
|
state.member_alliances.add(self.alliance)
|
||||||
|
state.member_corporations.add(self.corp)
|
||||||
|
|
||||||
|
self.member = AuthUtils.create_user('test user')
|
||||||
|
self.member.profile.main_character = self.char
|
||||||
|
self.member.profile.save()
|
||||||
|
|
||||||
|
@patch('.signals.create_required_models')
|
||||||
|
def test_create_required_models_triggered_true(
|
||||||
|
self, create_required_models):
|
||||||
|
"""
|
||||||
|
Create a User object here,
|
||||||
|
to generate UserProfile models
|
||||||
|
"""
|
||||||
|
post_save.connect(create_required_models, sender=User)
|
||||||
|
AuthUtils.create_user('test_create_required_models_triggered')
|
||||||
|
self.assertTrue = create_required_models.called
|
||||||
|
self.assertEqual(create_required_models.call_count, 1)
|
||||||
|
|
||||||
|
user = User.objects.get(username='test_create_required_models_triggered')
|
||||||
|
self.assertIsNot(UserProfile.objects.get(user=user), False)
|
||||||
|
|
||||||
|
@patch('.signals.create_required_models')
|
||||||
|
def test_create_required_models_triggered_false(
|
||||||
|
self, create_required_models):
|
||||||
|
"""
|
||||||
|
Only call a User object Update here,
|
||||||
|
which does not need to generate UserProfile models
|
||||||
|
"""
|
||||||
|
post_save.connect(create_required_models, sender=User)
|
||||||
|
char = EveCharacter.objects.create(
|
||||||
|
character_id='1266',
|
||||||
|
character_name='test character2',
|
||||||
|
corporation_id='2345',
|
||||||
|
corporation_name='test corp',
|
||||||
|
corporation_ticker='tickr',
|
||||||
|
alliance_id='3456',
|
||||||
|
alliance_name='alliance name',
|
||||||
|
)
|
||||||
|
self.member.profile.main_character = char
|
||||||
|
self.member.profile.save()
|
||||||
|
|
||||||
|
self.assertTrue = create_required_models.called
|
||||||
|
self.assertEqual(create_required_models.call_count, 0)
|
||||||
|
self.assertIsNot(UserProfile.objects.get(user=self.member), False)
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
@@ -7,21 +6,21 @@ from . import views
|
|||||||
app_name = 'authentication'
|
app_name = 'authentication'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.index, name='index'),
|
path('', views.index, name='index'),
|
||||||
url(
|
path(
|
||||||
r'^account/login/$',
|
'account/login/',
|
||||||
TemplateView.as_view(template_name='public/login.html'),
|
TemplateView.as_view(template_name='public/login.html'),
|
||||||
name='login'
|
name='login'
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r'^account/characters/main/$',
|
'account/characters/main/',
|
||||||
views.main_character_change,
|
views.main_character_change,
|
||||||
name='change_main_character'
|
name='change_main_character'
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r'^account/characters/add/$',
|
'account/characters/add/',
|
||||||
views.add_character,
|
views.add_character,
|
||||||
name='add_character'
|
name='add_character'
|
||||||
),
|
),
|
||||||
url(r'^dashboard/$', views.dashboard, name='dashboard'),
|
path('dashboard/', views.dashboard, name='dashboard'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from allianceauth.corputils import urls
|
from allianceauth.corputils import urls
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'corputils'
|
app_name = 'corputils'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.corpstats_view, name='view'),
|
path('', views.corpstats_view, name='view'),
|
||||||
url(r'^add/$', views.corpstats_add, name='add'),
|
path('add/', views.corpstats_add, name='add'),
|
||||||
url(r'^(?P<corp_id>(\d)*)/$', views.corpstats_view, name='view_corp'),
|
path('<int:corp_id>/', views.corpstats_view, name='view_corp'),
|
||||||
url(r'^(?P<corp_id>(\d)+)/update/$', views.corpstats_update, name='update'),
|
path('<int:corp_id>/update/', views.corpstats_update, name='update'),
|
||||||
url(r'^search/$', views.corpstats_search, name='search'),
|
path('search/', views.corpstats_search, name='search'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required, permission_required,
|
|||||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from esi.decorators import token_required
|
from esi.decorators import token_required
|
||||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.10 on 2022-01-05 18:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('eveonline', '0015_factions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='evecharacter',
|
||||||
|
name='character_name',
|
||||||
|
field=models.CharField(db_index=True, max_length=254),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -25,6 +25,8 @@ DOOMHEIM_CORPORATION_ID = 1000001
|
|||||||
|
|
||||||
|
|
||||||
class EveFactionInfo(models.Model):
|
class EveFactionInfo(models.Model):
|
||||||
|
"""A faction in Eve Online."""
|
||||||
|
|
||||||
faction_id = models.PositiveIntegerField(unique=True, db_index=True)
|
faction_id = models.PositiveIntegerField(unique=True, db_index=True)
|
||||||
faction_name = models.CharField(max_length=254, unique=True)
|
faction_name = models.CharField(max_length=254, unique=True)
|
||||||
|
|
||||||
@@ -66,6 +68,8 @@ class EveFactionInfo(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class EveAllianceInfo(models.Model):
|
class EveAllianceInfo(models.Model):
|
||||||
|
"""An alliance in Eve Online."""
|
||||||
|
|
||||||
alliance_id = models.PositiveIntegerField(unique=True)
|
alliance_id = models.PositiveIntegerField(unique=True)
|
||||||
alliance_name = models.CharField(max_length=254, unique=True)
|
alliance_name = models.CharField(max_length=254, unique=True)
|
||||||
alliance_ticker = models.CharField(max_length=254)
|
alliance_ticker = models.CharField(max_length=254)
|
||||||
@@ -132,6 +136,8 @@ class EveAllianceInfo(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class EveCorporationInfo(models.Model):
|
class EveCorporationInfo(models.Model):
|
||||||
|
"""A corporation in Eve Online."""
|
||||||
|
|
||||||
corporation_id = models.PositiveIntegerField(unique=True)
|
corporation_id = models.PositiveIntegerField(unique=True)
|
||||||
corporation_name = models.CharField(max_length=254, unique=True)
|
corporation_name = models.CharField(max_length=254, unique=True)
|
||||||
corporation_ticker = models.CharField(max_length=254)
|
corporation_ticker = models.CharField(max_length=254)
|
||||||
@@ -195,9 +201,10 @@ class EveCorporationInfo(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class EveCharacter(models.Model):
|
class EveCharacter(models.Model):
|
||||||
"""Character in Eve Online"""
|
"""A 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, db_index=True)
|
||||||
corporation_id = models.PositiveIntegerField()
|
corporation_id = models.PositiveIntegerField()
|
||||||
corporation_name = models.CharField(max_length=254)
|
corporation_name = models.CharField(max_length=254)
|
||||||
corporation_ticker = models.CharField(max_length=5)
|
corporation_ticker = models.CharField(max_length=5)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def update_character(character_id: int) -> None:
|
|||||||
def run_model_update():
|
def run_model_update():
|
||||||
"""Update all alliances, corporations and characters from ESI"""
|
"""Update all alliances, corporations and characters from ESI"""
|
||||||
|
|
||||||
# update existing corp models
|
#update existing corp models
|
||||||
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
||||||
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
|
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from . import urls
|
from . import urls
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class FatlinkForm(forms.Form):
|
class FatlinkForm(forms.Form):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<form class="form-signin" role="form" action="" method="POST">
|
<form class="form-signin" role="form" action="" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit" name="submit_fat">{% translate "Create fatlink" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit" name="submit_fat">{% translate "Create fatlink" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
app_name = 'fleetactivitytracking'
|
app_name = 'fleetactivitytracking'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# FleetActivityTracking (FAT)
|
# FleetActivityTracking (FAT)
|
||||||
url(r'^$', views.fatlink_view, name='view'),
|
path('', views.fatlink_view, name='view'),
|
||||||
url(r'^statistics/$', views.fatlink_statistics_view, name='statistics'),
|
path('statistics/', views.fatlink_statistics_view, name='statistics'),
|
||||||
url(r'^statistics/corp/(\w+)$', views.fatlink_statistics_corp_view,
|
path('statistics/corp/<int:corpid>/', views.fatlink_statistics_corp_view,
|
||||||
name='statistics_corp'),
|
name='statistics_corp'),
|
||||||
url(r'^statistics/corp/(?P<corpid>\w+)/(?P<year>[0-9]+)/(?P<month>[0-9]+)/',
|
path('statistics/corp/<int:corpid>/<int:year>/<int:month>/',
|
||||||
views.fatlink_statistics_corp_view,
|
views.fatlink_statistics_corp_view,
|
||||||
name='statistics_corp_month'),
|
name='statistics_corp_month'),
|
||||||
url(r'^statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$', views.fatlink_statistics_view,
|
path('statistics/<int:year>/<int:month>/', views.fatlink_statistics_view,
|
||||||
name='statistics_month'),
|
name='statistics_month'),
|
||||||
url(r'^user/statistics/$', views.fatlink_personal_statistics_view,
|
path('user/statistics/', views.fatlink_personal_statistics_view,
|
||||||
name='personal_statistics'),
|
name='personal_statistics'),
|
||||||
url(r'^user/statistics/(?P<year>[0-9]+)/$', views.fatlink_personal_statistics_view,
|
path('user/statistics/<int:year>/', views.fatlink_personal_statistics_view,
|
||||||
name='personal_statistics_year'),
|
name='personal_statistics_year'),
|
||||||
url(r'^user/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$',
|
path('user/statistics/<int:year>/<int:month>/',
|
||||||
views.fatlink_monthly_personal_statistics_view,
|
views.fatlink_monthly_personal_statistics_view,
|
||||||
name='personal_statistics_month'),
|
name='personal_statistics_month'),
|
||||||
url(r'^user/(?P<char_id>[0-9]+)/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$',
|
path('user/<int:char_id>/statistics/<int:year>/<int:month>/',
|
||||||
views.fatlink_monthly_personal_statistics_view,
|
views.fatlink_monthly_personal_statistics_view,
|
||||||
name='user_statistics_month'),
|
name='user_statistics_month'),
|
||||||
url(r'^create/$', views.create_fatlink_view, name='create'),
|
path('create/', views.create_fatlink_view, name='create'),
|
||||||
url(r'^modify/(?P<fat_hash>[a-zA-Z0-9_-]+)/$', views.modify_fatlink_view, name='modify'),
|
path('modify/<str:fat_hash>/', views.modify_fatlink_view, name='modify'),
|
||||||
url(r'^link/(?P<fat_hash>[a-zA-Z0-9]+)/$', views.click_fatlink_view, name='click'),
|
path('link/<str:fat_hash>/', views.click_fatlink_view, name='click'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.shortcuts import render, redirect, get_object_or_404, Http404
|
from django.shortcuts import render, redirect, get_object_or_404, Http404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from esi.decorators import token_required
|
from esi.decorators import token_required
|
||||||
from allianceauth.eveonline.providers import provider
|
from allianceauth.eveonline.providers import provider
|
||||||
from .forms import FatlinkForm
|
from .forms import FatlinkForm
|
||||||
@@ -212,14 +212,7 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
|
|||||||
start_of_previous_month = first_day_of_previous_month(year, month)
|
start_of_previous_month = first_day_of_previous_month(year, month)
|
||||||
|
|
||||||
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
|
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
|
||||||
try:
|
user = EveCharacter.objects.get(character_id=char_id).user
|
||||||
user = EveCharacter.objects.get(character_id=char_id).character_ownership.user
|
|
||||||
except EveCharacter.DoesNotExist:
|
|
||||||
messages.error(request, _('Character does not exist'))
|
|
||||||
return redirect('fatlink:view')
|
|
||||||
except AttributeError:
|
|
||||||
messages.error(request, _('User does not exist'))
|
|
||||||
return redirect('fatlink:view')
|
|
||||||
else:
|
else:
|
||||||
user = request.user
|
user = request.user
|
||||||
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")
|
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
|
from django import forms
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
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, Permission, User
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Count, Exists, OuterRef
|
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 (
|
from django.db.models.signals import pre_save, post_save, pre_delete, \
|
||||||
m2m_changed,
|
post_delete, m2m_changed
|
||||||
post_delete,
|
|
||||||
post_save,
|
|
||||||
pre_delete,
|
|
||||||
pre_save
|
|
||||||
)
|
|
||||||
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 .forms import GroupAdminForm, ReservedGroupNameAdminForm
|
from .models import AuthGroup, ReservedGroupName
|
||||||
from .models import AuthGroup, GroupRequest, ReservedGroupName
|
from .models import GroupRequest
|
||||||
from .tasks import remove_users_not_matching_states_from_group
|
|
||||||
|
|
||||||
if 'eve_autogroups' in apps.app_configs:
|
if 'eve_autogroups' in apps.app_configs:
|
||||||
_has_auto_groups = True
|
_has_auto_groups = True
|
||||||
@@ -30,12 +28,10 @@ class AuthGroupInlineAdmin(admin.StackedInline):
|
|||||||
'description',
|
'description',
|
||||||
'group_leaders',
|
'group_leaders',
|
||||||
'group_leader_groups',
|
'group_leader_groups',
|
||||||
'states',
|
'states', 'internal',
|
||||||
'internal',
|
|
||||||
'hidden',
|
'hidden',
|
||||||
'open',
|
'open',
|
||||||
'public',
|
'public'
|
||||||
'restricted',
|
|
||||||
)
|
)
|
||||||
verbose_name_plural = 'Auth Settings'
|
verbose_name_plural = 'Auth Settings'
|
||||||
verbose_name = ''
|
verbose_name = ''
|
||||||
@@ -54,11 +50,6 @@ class AuthGroupInlineAdmin(admin.StackedInline):
|
|||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
return request.user.has_perm('auth.change_group')
|
return request.user.has_perm('auth.change_group')
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
return self.readonly_fields + ("restricted",)
|
|
||||||
return self.readonly_fields
|
|
||||||
|
|
||||||
|
|
||||||
if _has_auto_groups:
|
if _has_auto_groups:
|
||||||
class IsAutoGroupFilter(admin.SimpleListFilter):
|
class IsAutoGroupFilter(admin.SimpleListFilter):
|
||||||
@@ -105,15 +96,27 @@ class HasLeaderFilter(admin.SimpleListFilter):
|
|||||||
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
|
form = GroupAdminForm
|
||||||
|
list_select_related = ('authgroup',)
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
list_display = (
|
list_display = (
|
||||||
'name',
|
'name',
|
||||||
'_description',
|
'_description',
|
||||||
'_properties',
|
'_properties',
|
||||||
'_member_count',
|
'_member_count',
|
||||||
'has_leader',
|
'has_leader'
|
||||||
)
|
)
|
||||||
list_filter = [
|
list_filter = [
|
||||||
'authgroup__internal',
|
'authgroup__internal',
|
||||||
@@ -129,51 +132,34 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
has_leader_qs = (
|
|
||||||
AuthGroup.objects.filter(group=OuterRef('pk'), group_leaders__isnull=False)
|
|
||||||
)
|
|
||||||
has_leader_groups_qs = (
|
|
||||||
AuthGroup.objects.filter(
|
|
||||||
group=OuterRef('pk'), group_leader_groups__isnull=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
qs = (
|
|
||||||
qs.select_related('authgroup')
|
|
||||||
.annotate(member_count=Count('user', distinct=True))
|
|
||||||
.annotate(has_leader=Exists(has_leader_qs))
|
|
||||||
.annotate(has_leader_groups=Exists(has_leader_groups_qs))
|
|
||||||
)
|
|
||||||
if _has_auto_groups:
|
if _has_auto_groups:
|
||||||
is_autogroup_corp = (
|
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
|
||||||
Group.objects.filter(
|
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
|
||||||
pk=OuterRef('pk'), managedcorpgroup__isnull=False
|
qs = qs.annotate(
|
||||||
)
|
member_count=Count('user', distinct=True),
|
||||||
)
|
)
|
||||||
is_autogroup_alliance = (
|
|
||||||
Group.objects.filter(
|
|
||||||
pk=OuterRef('pk'), managedalliancegroup__isnull=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
qs = (
|
|
||||||
qs.annotate(is_autogroup_corp=Exists(is_autogroup_corp))
|
|
||||||
.annotate(is_autogroup_alliance=Exists(is_autogroup_alliance))
|
|
||||||
)
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _description(self, obj):
|
def _description(self, obj):
|
||||||
return obj.authgroup.description
|
return obj.authgroup.description
|
||||||
|
|
||||||
@admin.display(description='Members', ordering='member_count')
|
|
||||||
def _member_count(self, obj):
|
def _member_count(self, obj):
|
||||||
return obj.member_count
|
return obj.member_count
|
||||||
|
|
||||||
@admin.display(boolean=True)
|
_member_count.short_description = 'Members'
|
||||||
|
_member_count.admin_order_field = 'member_count'
|
||||||
|
|
||||||
def has_leader(self, obj):
|
def has_leader(self, obj):
|
||||||
return obj.has_leader or obj.has_leader_groups
|
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
|
||||||
|
|
||||||
|
has_leader.boolean = True
|
||||||
|
|
||||||
def _properties(self, obj):
|
def _properties(self, obj):
|
||||||
properties = list()
|
properties = list()
|
||||||
if _has_auto_groups and (obj.is_autogroup_corp or obj.is_autogroup_alliance):
|
if _has_auto_groups and (
|
||||||
|
obj.managedalliancegroup_set.exists()
|
||||||
|
or obj.managedcorpgroup_set.exists()
|
||||||
|
):
|
||||||
properties.append('Auto Group')
|
properties.append('Auto Group')
|
||||||
elif obj.authgroup.internal:
|
elif obj.authgroup.internal:
|
||||||
properties.append('Internal')
|
properties.append('Internal')
|
||||||
@@ -186,10 +172,11 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
properties.append('Public')
|
properties.append('Public')
|
||||||
if not properties:
|
if not properties:
|
||||||
properties.append('Default')
|
properties.append('Default')
|
||||||
if obj.authgroup.restricted:
|
|
||||||
properties.append('Restricted')
|
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
_properties.short_description = "properties"
|
||||||
|
|
||||||
filter_horizontal = ('permissions',)
|
filter_horizontal = ('permissions',)
|
||||||
inlines = (AuthGroupInlineAdmin,)
|
inlines = (AuthGroupInlineAdmin,)
|
||||||
|
|
||||||
@@ -203,15 +190,8 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
ag_instance = inline_form.save(commit=False)
|
ag_instance = inline_form.save(commit=False)
|
||||||
ag_instance.group = form.instance
|
ag_instance.group = form.instance
|
||||||
ag_instance.save()
|
ag_instance.save()
|
||||||
if ag_instance.states.exists():
|
|
||||||
remove_users_not_matching_states_from_group.delay(ag_instance.group.pk)
|
|
||||||
formset.save()
|
formset.save()
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
return self.readonly_fields + ("permissions",)
|
|
||||||
return self.readonly_fields
|
|
||||||
|
|
||||||
|
|
||||||
class Group(BaseGroup):
|
class Group(BaseGroup):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -236,10 +216,33 @@ class GroupRequestAdmin(admin.ModelAdmin):
|
|||||||
'leave_request',
|
'leave_request',
|
||||||
)
|
)
|
||||||
|
|
||||||
@admin.display(boolean=True, description="is leave request")
|
|
||||||
def _leave_request(self, obj) -> True:
|
def _leave_request(self, obj) -> True:
|
||||||
return obj.leave_request
|
return obj.leave_request
|
||||||
|
|
||||||
|
_leave_request.short_description = 'is leave request'
|
||||||
|
_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)
|
@admin.register(ReservedGroupName)
|
||||||
class ReservedGroupNameAdmin(admin.ModelAdmin):
|
class ReservedGroupNameAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .models import ReservedGroupName
|
|
||||||
|
|
||||||
|
|
||||||
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 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()
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.10 on 2022-04-08 19:30
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('groupmanagement', '0018_reservedgroupname'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='authgroup',
|
|
||||||
name='restricted',
|
|
||||||
field=models.BooleanField(default=False, help_text='Group is restricted. This means that adding or removing users for this group requires a superuser admin.'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -13,7 +13,6 @@ from allianceauth.notifications import notify
|
|||||||
|
|
||||||
class GroupRequest(models.Model):
|
class GroupRequest(models.Model):
|
||||||
"""Request from a user for joining or leaving a group."""
|
"""Request from a user for joining or leaving a group."""
|
||||||
|
|
||||||
leave_request = models.BooleanField(default=0)
|
leave_request = models.BooleanField(default=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)
|
||||||
@@ -45,7 +44,6 @@ class GroupRequest(models.Model):
|
|||||||
|
|
||||||
class RequestLog(models.Model):
|
class RequestLog(models.Model):
|
||||||
"""Log entry about who joined and left a group and who approved it."""
|
"""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)
|
||||||
@@ -97,7 +95,6 @@ class AuthGroup(models.Model):
|
|||||||
Open - Users are automatically accepted into the group
|
Open - Users are automatically accepted into the group
|
||||||
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(
|
internal = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
@@ -129,13 +126,6 @@ class AuthGroup(models.Model):
|
|||||||
"are no longer authenticated."
|
"are no longer authenticated."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
restricted = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text=_(
|
|
||||||
"Group is restricted. This means that adding or removing users "
|
|
||||||
"for this group requires a superuser admin."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
group_leaders = models.ManyToManyField(
|
group_leaders = models.ManyToManyField(
|
||||||
User,
|
User,
|
||||||
related_name='leads_groups',
|
related_name='leads_groups',
|
||||||
@@ -189,22 +179,12 @@ class AuthGroup(models.Model):
|
|||||||
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
|
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
|
||||||
)
|
)
|
||||||
|
|
||||||
def remove_users_not_matching_states(self):
|
|
||||||
"""Remove users not matching defined states from related group."""
|
|
||||||
states_qs = self.states.all()
|
|
||||||
if states_qs.exists():
|
|
||||||
states = list(states_qs)
|
|
||||||
non_compliant_users = self.group.user_set.exclude(profile__state__in=states)
|
|
||||||
for user in non_compliant_users:
|
|
||||||
self.group.user_set.remove(user)
|
|
||||||
|
|
||||||
|
|
||||||
class ReservedGroupName(models.Model):
|
class ReservedGroupName(models.Model):
|
||||||
"""Name that can not be used for groups.
|
"""Name that can not be used for groups.
|
||||||
|
|
||||||
This enables AA to ignore groups on other services (e.g. Discord) with that name.
|
This enables AA to ignore groups on other services (e.g. Discord) with that name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
_('name'),
|
_('name'),
|
||||||
max_length=150,
|
max_length=150,
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
from celery import shared_task
|
|
||||||
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def remove_users_not_matching_states_from_group(group_pk: int) -> None:
|
|
||||||
"""Remove users not matching defined states from related group."""
|
|
||||||
group = Group.objects.get(pk=group_pk)
|
|
||||||
group.authgroup.remove_users_not_matching_states()
|
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django_webtest import WebTest
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase, RequestFactory, Client, override_settings
|
from django.test import TestCase, RequestFactory, Client
|
||||||
|
|
||||||
from allianceauth.authentication.models import CharacterOwnership, State
|
from allianceauth.authentication.models import CharacterOwnership, State
|
||||||
from allianceauth.eveonline.models import (
|
from allianceauth.eveonline.models import (
|
||||||
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||||
)
|
)
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
|
||||||
|
|
||||||
from . import get_admin_change_view_url
|
|
||||||
from ..admin import HasLeaderFilter, GroupAdmin, Group
|
from ..admin import HasLeaderFilter, GroupAdmin, Group
|
||||||
|
from . import get_admin_change_view_url
|
||||||
from ..models import ReservedGroupName
|
from ..models import ReservedGroupName
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +33,7 @@ class MockRequest:
|
|||||||
|
|
||||||
|
|
||||||
class TestGroupAdmin(TestCase):
|
class TestGroupAdmin(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
@@ -236,104 +233,60 @@ class TestGroupAdmin(TestCase):
|
|||||||
self.assertEqual(result, expected)
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
def test_member_count(self):
|
def test_member_count(self):
|
||||||
# given
|
expected = 1
|
||||||
request = MockRequest(user=self.user_1)
|
obj = self.modeladmin.get_queryset(MockRequest(user=self.user_1))\
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
.get(pk=self.group_1.pk)
|
||||||
# when
|
|
||||||
result = self.modeladmin._member_count(obj)
|
result = self.modeladmin._member_count(obj)
|
||||||
# then
|
self.assertEqual(result, expected)
|
||||||
self.assertEqual(result, 1)
|
|
||||||
|
|
||||||
def test_has_leader_user(self):
|
def test_has_leader_user(self):
|
||||||
# given
|
result = self.modeladmin.has_leader(self.group_1)
|
||||||
request = MockRequest(user=self.user_1)
|
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
|
||||||
# when
|
|
||||||
result = self.modeladmin.has_leader(obj)
|
|
||||||
# then
|
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_has_leader_group(self):
|
def test_has_leader_group(self):
|
||||||
# given
|
result = self.modeladmin.has_leader(self.group_2)
|
||||||
request = MockRequest(user=self.user_1)
|
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
|
|
||||||
# when
|
|
||||||
result = self.modeladmin.has_leader(obj)
|
|
||||||
# then
|
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_properties_1(self):
|
def test_properties_1(self):
|
||||||
# given
|
expected = ['Default']
|
||||||
request = MockRequest(user=self.user_1)
|
result = self.modeladmin._properties(self.group_1)
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
self.assertListEqual(result, expected)
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
self.assertListEqual(result, ['Default'])
|
|
||||||
|
|
||||||
def test_properties_2(self):
|
def test_properties_2(self):
|
||||||
# given
|
expected = ['Internal']
|
||||||
request = MockRequest(user=self.user_1)
|
result = self.modeladmin._properties(self.group_2)
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
|
self.assertListEqual(result, expected)
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
self.assertListEqual(result, ['Internal'])
|
|
||||||
|
|
||||||
def test_properties_3(self):
|
def test_properties_3(self):
|
||||||
# given
|
expected = ['Hidden']
|
||||||
request = MockRequest(user=self.user_1)
|
result = self.modeladmin._properties(self.group_3)
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_3.pk)
|
self.assertListEqual(result, expected)
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
self.assertListEqual(result, ['Hidden'])
|
|
||||||
|
|
||||||
def test_properties_4(self):
|
def test_properties_4(self):
|
||||||
# given
|
expected = ['Open']
|
||||||
request = MockRequest(user=self.user_1)
|
result = self.modeladmin._properties(self.group_4)
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_4.pk)
|
self.assertListEqual(result, expected)
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
self.assertListEqual(result, ['Open'])
|
|
||||||
|
|
||||||
def test_properties_5(self):
|
def test_properties_5(self):
|
||||||
# given
|
expected = ['Public']
|
||||||
request = MockRequest(user=self.user_1)
|
result = self.modeladmin._properties(self.group_5)
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_5.pk)
|
self.assertListEqual(result, expected)
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
self.assertListEqual(result, ['Public'])
|
|
||||||
|
|
||||||
def test_properties_6(self):
|
def test_properties_6(self):
|
||||||
# given
|
expected = ['Hidden', 'Open', 'Public']
|
||||||
request = MockRequest(user=self.user_1)
|
result = self.modeladmin._properties(self.group_6)
|
||||||
obj = self.modeladmin.get_queryset(request).get(pk=self.group_6.pk)
|
self.assertListEqual(result, expected)
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
self.assertListEqual(result, ['Hidden', 'Open', 'Public'])
|
|
||||||
|
|
||||||
if _has_auto_groups:
|
if _has_auto_groups:
|
||||||
@patch(MODULE_PATH + '._has_auto_groups', True)
|
@patch(MODULE_PATH + '._has_auto_groups', True)
|
||||||
def test_should_show_autogroup_for_corporation(self):
|
def test_properties_7(self):
|
||||||
# given
|
|
||||||
self._create_autogroups()
|
self._create_autogroups()
|
||||||
request = MockRequest(user=self.user_1)
|
expected = ['Auto Group']
|
||||||
queryset = self.modeladmin.get_queryset(request)
|
my_group = Group.objects\
|
||||||
obj = queryset.filter(managedcorpgroup__isnull=False).first()
|
.filter(managedcorpgroup__isnull=False)\
|
||||||
# when
|
.first()
|
||||||
result = self.modeladmin._properties(obj)
|
result = self.modeladmin._properties(my_group)
|
||||||
# then
|
self.assertListEqual(result, expected)
|
||||||
self.assertListEqual(result, ['Auto Group'])
|
|
||||||
|
|
||||||
@patch(MODULE_PATH + '._has_auto_groups', True)
|
|
||||||
def test_should_show_autogroup_for_alliance(self):
|
|
||||||
# given
|
|
||||||
self._create_autogroups()
|
|
||||||
request = MockRequest(user=self.user_1)
|
|
||||||
queryset = self.modeladmin.get_queryset(request)
|
|
||||||
obj = queryset.filter(managedalliancegroup__isnull=False).first()
|
|
||||||
# when
|
|
||||||
result = self.modeladmin._properties(obj)
|
|
||||||
# then
|
|
||||||
self.assertListEqual(result, ['Auto Group'])
|
|
||||||
|
|
||||||
# actions
|
# actions
|
||||||
|
|
||||||
@@ -515,136 +468,6 @@ class TestGroupAdmin(TestCase):
|
|||||||
self.assertFalse(Group.objects.filter(name="new group").exists())
|
self.assertFalse(Group.objects.filter(name="new group").exists())
|
||||||
|
|
||||||
|
|
||||||
class TestGroupAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
super().setUpClass()
|
|
||||||
cls.super_admin = User.objects.create_superuser("super_admin")
|
|
||||||
cls.staff_admin = User.objects.create_user("staff_admin")
|
|
||||||
cls.staff_admin.is_staff = True
|
|
||||||
cls.staff_admin.save()
|
|
||||||
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
|
|
||||||
[
|
|
||||||
"auth.add_group",
|
|
||||||
"auth.change_group",
|
|
||||||
"auth.view_group",
|
|
||||||
"groupmanagement.add_group",
|
|
||||||
"groupmanagement.change_group",
|
|
||||||
"groupmanagement.view_group",
|
|
||||||
],
|
|
||||||
cls.staff_admin
|
|
||||||
)
|
|
||||||
cls.superuser_exclusive_fields = ["permissions", "authgroup-0-restricted"]
|
|
||||||
|
|
||||||
def test_should_show_all_fields_to_superuser_for_add(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
page = self.app.get("/admin/groupmanagement/group/add/")
|
|
||||||
# when
|
|
||||||
form = page.forms["group_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_not_show_all_fields_to_staff_admins_for_add(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
page = self.app.get("/admin/groupmanagement/group/add/")
|
|
||||||
# when
|
|
||||||
form = page.forms["group_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertNotIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_show_all_fields_to_superuser_for_change(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.super_admin)
|
|
||||||
group = Group.objects.create(name="Dummy group")
|
|
||||||
page = self.app.get(f"/admin/groupmanagement/group/{group.pk}/change/")
|
|
||||||
# when
|
|
||||||
form = page.forms["group_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertIn(field, form.fields)
|
|
||||||
|
|
||||||
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
|
|
||||||
# given
|
|
||||||
self.app.set_user(self.staff_admin)
|
|
||||||
group = Group.objects.create(name="Dummy group")
|
|
||||||
page = self.app.get(f"/admin/groupmanagement/group/{group.pk}/change/")
|
|
||||||
# when
|
|
||||||
form = page.forms["group_form"]
|
|
||||||
# then
|
|
||||||
for field in self.superuser_exclusive_fields:
|
|
||||||
with self.subTest(field=field):
|
|
||||||
self.assertNotIn(field, form.fields)
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
|
||||||
class TestGroupAdmin2(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
super().setUpClass()
|
|
||||||
cls.superuser = User.objects.create_superuser("super")
|
|
||||||
|
|
||||||
def test_should_remove_users_from_state_groups(self):
|
|
||||||
# given
|
|
||||||
user_member = AuthUtils.create_user("Bruce Wayne")
|
|
||||||
character_member = AuthUtils.add_main_character_2(
|
|
||||||
user_member,
|
|
||||||
name="Bruce Wayne",
|
|
||||||
character_id=1001,
|
|
||||||
corp_id=2001,
|
|
||||||
corp_name="Wayne Technologies",
|
|
||||||
)
|
|
||||||
user_guest = AuthUtils.create_user("Lex Luthor")
|
|
||||||
AuthUtils.add_main_character_2(
|
|
||||||
user_guest,
|
|
||||||
name="Lex Luthor",
|
|
||||||
character_id=1011,
|
|
||||||
corp_id=2011,
|
|
||||||
corp_name="Luthor Corp",
|
|
||||||
)
|
|
||||||
member_state = AuthUtils.get_member_state()
|
|
||||||
member_state.member_characters.add(character_member)
|
|
||||||
user_member.refresh_from_db()
|
|
||||||
user_guest.refresh_from_db()
|
|
||||||
group = Group.objects.create(name="dummy")
|
|
||||||
user_member.groups.add(group)
|
|
||||||
user_guest.groups.add(group)
|
|
||||||
group.authgroup.states.add(member_state)
|
|
||||||
self.client.force_login(self.superuser)
|
|
||||||
# when
|
|
||||||
response = self.client.post(
|
|
||||||
f"/admin/groupmanagement/group/{group.pk}/change/",
|
|
||||||
data={
|
|
||||||
"name": f"{group.name}",
|
|
||||||
"authgroup-TOTAL_FORMS": "1",
|
|
||||||
"authgroup-INITIAL_FORMS": "1",
|
|
||||||
"authgroup-MIN_NUM_FORMS": "0",
|
|
||||||
"authgroup-MAX_NUM_FORMS": "1",
|
|
||||||
"authgroup-0-description": "",
|
|
||||||
"authgroup-0-states": f"{member_state.pk}",
|
|
||||||
"authgroup-0-internal": "on",
|
|
||||||
"authgroup-0-hidden": "on",
|
|
||||||
"authgroup-0-group": f"{group.pk}",
|
|
||||||
"authgroup-__prefix__-description": "",
|
|
||||||
"authgroup-__prefix__-internal": "on",
|
|
||||||
"authgroup-__prefix__-hidden": "on",
|
|
||||||
"authgroup-__prefix__-group": f"{group.pk}",
|
|
||||||
"_save": "Save"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# then
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
|
||||||
self.assertIn(group, user_member.groups.all())
|
|
||||||
self.assertNotIn(group, user_guest.groups.all())
|
|
||||||
|
|
||||||
|
|
||||||
class TestReservedGroupNameAdmin(TestCase):
|
class TestReservedGroupNameAdmin(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|||||||
@@ -232,38 +232,6 @@ class TestAuthGroup(TestCase):
|
|||||||
expected = 'Superheros'
|
expected = 'Superheros'
|
||||||
self.assertEqual(str(group.authgroup), expected)
|
self.assertEqual(str(group.authgroup), expected)
|
||||||
|
|
||||||
def test_should_remove_guests_from_group_when_restricted_to_members_only(self):
|
|
||||||
# given
|
|
||||||
user_member = AuthUtils.create_user("Bruce Wayne")
|
|
||||||
character_member = AuthUtils.add_main_character_2(
|
|
||||||
user_member,
|
|
||||||
name="Bruce Wayne",
|
|
||||||
character_id=1001,
|
|
||||||
corp_id=2001,
|
|
||||||
corp_name="Wayne Technologies",
|
|
||||||
)
|
|
||||||
user_guest = AuthUtils.create_user("Lex Luthor")
|
|
||||||
AuthUtils.add_main_character_2(
|
|
||||||
user_guest,
|
|
||||||
name="Lex Luthor",
|
|
||||||
character_id=1011,
|
|
||||||
corp_id=2011,
|
|
||||||
corp_name="Luthor Corp",
|
|
||||||
)
|
|
||||||
member_state = AuthUtils.get_member_state()
|
|
||||||
member_state.member_characters.add(character_member)
|
|
||||||
user_member.refresh_from_db()
|
|
||||||
user_guest.refresh_from_db()
|
|
||||||
group = Group.objects.create(name="dummy")
|
|
||||||
user_member.groups.add(group)
|
|
||||||
user_guest.groups.add(group)
|
|
||||||
group.authgroup.states.add(member_state)
|
|
||||||
# when
|
|
||||||
group.authgroup.remove_users_not_matching_states()
|
|
||||||
# then
|
|
||||||
self.assertIn(group, user_member.groups.all())
|
|
||||||
self.assertNotIn(group, user_guest.groups.all())
|
|
||||||
|
|
||||||
|
|
||||||
class TestAuthGroupRequestApprovers(TestCase):
|
class TestAuthGroupRequestApprovers(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|||||||
@@ -1,51 +1,50 @@
|
|||||||
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
app_name = "groupmanagement"
|
app_name = "groupmanagement"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# groups
|
# groups
|
||||||
url(r"^groups/$", views.groups_view, name="groups"),
|
path("groups", views.groups_view, name="groups"),
|
||||||
url(r"^group/request/join/(\w+)/$", views.group_request_add, name="request_add"),
|
path("group/request/join/<int:group_id>/", views.group_request_add, name="request_add"),
|
||||||
url(
|
path(
|
||||||
r"^group/request/leave/(\w+)/$", views.group_request_leave, name="request_leave"
|
"group/request/leave/<int:group_id>/", views.group_request_leave, name="request_leave"
|
||||||
),
|
),
|
||||||
# group management
|
# group management
|
||||||
url(r"^groupmanagement/requests/$", views.group_management, name="management"),
|
path("groupmanagement/requests/", views.group_management, name="management"),
|
||||||
url(r"^groupmanagement/membership/$", views.group_membership, name="membership"),
|
path("groupmanagement/membership/", views.group_membership, name="membership"),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/membership/(\w+)/$",
|
"groupmanagement/membership/<int:group_id>/",
|
||||||
views.group_membership_list,
|
views.group_membership_list,
|
||||||
name="membership",
|
name="membership",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/membership/(\w+)/audit-log/$",
|
"groupmanagement/membership/<int:group_id>/audit-log/",
|
||||||
views.group_membership_audit,
|
views.group_membership_audit,
|
||||||
name="audit_log",
|
name="audit_log",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/membership/(\w+)/remove/(\w+)/$",
|
"groupmanagement/membership/<int:group_id>/remove/<int:user_id>/",
|
||||||
views.group_membership_remove,
|
views.group_membership_remove,
|
||||||
name="membership_remove",
|
name="membership_remove",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/request/join/accept/(\w+)/$",
|
"groupmanagement/request/join/accept/<int:group_request_id>/",
|
||||||
views.group_accept_request,
|
views.group_accept_request,
|
||||||
name="accept_request",
|
name="accept_request",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/request/join/reject/(\w+)/$",
|
"groupmanagement/request/join/reject/<int:group_request_id>/",
|
||||||
views.group_reject_request,
|
views.group_reject_request,
|
||||||
name="reject_request",
|
name="reject_request",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/request/leave/accept/(\w+)/$",
|
"groupmanagement/request/leave/accept/<int:group_request_id>/",
|
||||||
views.group_leave_accept_request,
|
views.group_leave_accept_request,
|
||||||
name="leave_accept_request",
|
name="leave_accept_request",
|
||||||
),
|
),
|
||||||
url(
|
path(
|
||||||
r"^groupmanagement/request/leave/reject/(\w+)/$",
|
"groupmanagement/request/leave/reject/<int:group_request_id>/",
|
||||||
views.group_leave_reject_request,
|
views.group_leave_reject_request,
|
||||||
name="leave_reject_request",
|
name="leave_reject_request",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth.notifications import notify
|
from allianceauth.notifications import notify
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class HRApplicationCommentForm(forms.Form):
|
class HRApplicationCommentForm(forms.Form):
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
<div cass="text-center">{{ question.help_text }}</div>
|
<div cass="text-center">{{ question.help_text }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for choice in question.choices.all %}
|
{% for choice in question.choices.all %}
|
||||||
<input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
|
<input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}">
|
||||||
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
|
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
|
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
|
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ search_form|bootstrap }}
|
{{ search_form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
|
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ search_form|bootstrap }}
|
{{ search_form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<form class="form-signin" role="form" action="" method="POST">
|
<form class="form-signin" role="form" action="" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ comment_form|bootstrap }}
|
{{ comment_form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Add Comment" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Add Comment" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'hrapplications'
|
app_name = 'hrapplications'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.hr_application_management_view,
|
path('', views.hr_application_management_view,
|
||||||
name="index"),
|
name="index"),
|
||||||
url(r'^create/$', views.hr_application_create_view,
|
path('create/', views.hr_application_create_view,
|
||||||
name="create_view"),
|
name="create_view"),
|
||||||
url(r'^create/(\d+)', views.hr_application_create_view,
|
path('create/<int:form_id>/', views.hr_application_create_view,
|
||||||
name="create_view"),
|
name="create_view"),
|
||||||
url(r'^remove/(\w+)', views.hr_application_remove,
|
path('remove/<int:app_id>/', views.hr_application_remove,
|
||||||
name="remove"),
|
name="remove"),
|
||||||
url(r'^view/(\w+)', views.hr_application_view,
|
path('view/<int:app_id>/', views.hr_application_view,
|
||||||
name="view"),
|
name="view"),
|
||||||
url(r'^personal/view/(\w+)', views.hr_application_personal_view,
|
path('personal/view/<int:app_id>/', views.hr_application_personal_view,
|
||||||
name="personal_view"),
|
name="personal_view"),
|
||||||
url(r'^personal/removal/(\w+)',
|
path('personal/removal/<int:app_id>/',
|
||||||
views.hr_application_personal_removal,
|
views.hr_application_personal_removal,
|
||||||
name="personal_removal"),
|
name="personal_removal"),
|
||||||
url(r'^approve/(\w+)', views.hr_application_approve,
|
path('approve/<int:app_id>/', views.hr_application_approve,
|
||||||
name="approve"),
|
name="approve"),
|
||||||
url(r'^reject/(\w+)', views.hr_application_reject,
|
path('reject/<int:app_id>/', views.hr_application_reject,
|
||||||
name="reject"),
|
name="reject"),
|
||||||
url(r'^search/', views.hr_application_search,
|
path('search/', views.hr_application_search,
|
||||||
name="search"),
|
name="search"),
|
||||||
url(r'^mark_in_progress/(\w+)', views.hr_application_mark_in_progress,
|
path('mark_in_progress/<int:app_id>/', views.hr_application_mark_in_progress,
|
||||||
name="mark_in_progress"),
|
name="mark_in_progress"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
from .core import notify # noqa: F401
|
|
||||||
|
|
||||||
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
|
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
|
||||||
|
|
||||||
|
|
||||||
|
def notify(
|
||||||
|
user: object, title: str, message: str = None, level: str = 'info'
|
||||||
|
) -> None:
|
||||||
|
"""Sends a new notification to user. Convenience function to manager pendant."""
|
||||||
|
from .models import Notification
|
||||||
|
Notification.objects.notify_user(user, title, message, level)
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
class NotifyApiWrapper:
|
|
||||||
"""Wrapper to create notify API."""
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs): # provide old API for backwards compatibility
|
|
||||||
return self._add_notification(*args, **kwargs)
|
|
||||||
|
|
||||||
def danger(self, user: object, title: str, message: str = None) -> None:
|
|
||||||
"""Add danger notification for user."""
|
|
||||||
self._add_notification(user, title, message, level="danger")
|
|
||||||
|
|
||||||
def info(self, user: object, title: str, message: str = None) -> None:
|
|
||||||
"""Add info notification for user."""
|
|
||||||
self._add_notification(user=user, title=title, message=message, level="info")
|
|
||||||
|
|
||||||
def success(self, user: object, title: str, message: str = None) -> None:
|
|
||||||
"""Add success notification for user."""
|
|
||||||
self._add_notification(user, title, message, level="success")
|
|
||||||
|
|
||||||
def warning(self, user: object, title: str, message: str = None) -> None:
|
|
||||||
"""Add warning notification for user."""
|
|
||||||
self._add_notification(user, title, message, level="warning")
|
|
||||||
|
|
||||||
def _add_notification(
|
|
||||||
self, user: object, title: str, message: str = None, level: str = "info"
|
|
||||||
) -> None:
|
|
||||||
from .models import Notification
|
|
||||||
|
|
||||||
Notification.objects.notify_user(
|
|
||||||
user=user, title=title, message=message, level=level
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
@@ -5,34 +5,91 @@
|
|||||||
{% block page_title %}{% translate "Notifications" %}{% endblock %}
|
{% block page_title %}{% translate "Notifications" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-header text-center">{% translate "Notifications" %}</h1>
|
<div class="col-lg-12">
|
||||||
|
<h1 class="page-header text-center">{% translate "Notifications" %}</h1>
|
||||||
<div class="panel panel-default">
|
<div class="col-lg-12 container" id="example">
|
||||||
|
<div class="row">
|
||||||
<div class="panel-heading">
|
<div class="col-lg-12">
|
||||||
<ul class="nav nav-pills">
|
<div class="panel panel-default">
|
||||||
<li class="active"><a data-toggle="tab" href="#unread">{% translate "Unread" %}<b>({{ unread|length }})</b></a></li>
|
<div class="panel-heading">
|
||||||
<li><a data-toggle="tab" href="#read">{% translate "Read" %} <b>({{ read|length }})</b></a></li>
|
<ul class="nav nav-pills">
|
||||||
<div class="pull-right">
|
<li class="active"><a data-toggle="pill" href="#unread">{% translate "Unread" %}
|
||||||
<a href="{% url 'notifications:mark_all_read' %}" class="btn btn-warning">{% translate "Mark All Read" %}</a>
|
<b>({{ unread|length }})</b></a></li>
|
||||||
<a href="{% url 'notifications:delete_all_read' %}" class="btn btn-danger">{% translate "Delete All Read" %}</a>
|
<li><a data-toggle="pill" href="#read">{% translate "Read" %} <b>({{ read|length }})</b></a>
|
||||||
|
</li>
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="{% url 'notifications:mark_all_read' %}" class="btn btn-primary">{% translate "Mark All Read" %}</a>
|
||||||
|
<a href="{% url 'notifications:delete_all_read' %}" class="btn btn-danger">{% translate "Delete All Read" %}</a>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div id="unread" class="tab-pane fade in active">
|
||||||
|
<div class="table-responsive">
|
||||||
|
{% if unread %}
|
||||||
|
<table class="table table-condensed table-hover table-striped">
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">{% translate "Timestamp" %}</th>
|
||||||
|
<th class="text-center">{% translate "Title" %}</th>
|
||||||
|
<th class="text-center">{% translate "Action" %}</th>
|
||||||
|
</tr>
|
||||||
|
{% for notif in unread %}
|
||||||
|
<tr class="{{ notif.level }}">
|
||||||
|
<td class="text-center">{{ notif.timestamp }}</td>
|
||||||
|
<td class="text-center">{{ notif.title }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{% url 'notifications:view' notif.id %}" class="btn btn-success" title="View">
|
||||||
|
<span class="glyphicon glyphicon-eye-open"></span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notifications:remove' notif.id %}" class="btn btn-danger" title="Remove">
|
||||||
|
<span class="glyphicon glyphicon-remove"></span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning text-center">{% translate "No unread notifications." %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="read" class="tab-pane fade">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
{% if read %}
|
||||||
|
<table class="table table-condensed table-hover table-striped">
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">{% translate "Timestamp" %}</th>
|
||||||
|
<th class="text-center">{% translate "Title" %}</th>
|
||||||
|
<th class="text-center">{% translate "Action" %}</th>
|
||||||
|
</tr>
|
||||||
|
{% for notif in read %}
|
||||||
|
<tr class="{{ notif.level }}">
|
||||||
|
<td class="text-center">{{ notif.timestamp }}</td>
|
||||||
|
<td class="text-center">{{ notif.title }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{% url 'notifications:view' notif.id %}" class="btn btn-success" title="View">
|
||||||
|
<span class="glyphicon glyphicon-eye-open"></span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notifications:remove' notif.id %}" class="btn btn-danger" title="remove">
|
||||||
|
<span class="glyphicon glyphicon-remove"></span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning text-center">{% translate "No read notifications." %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="tab-content">
|
|
||||||
|
|
||||||
<div id="unread" class="tab-pane fade in active">
|
|
||||||
{% include "notifications/list_partial.html" with notifications=unread %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="read" class="tab-pane fade">
|
|
||||||
{% include "notifications/list_partial.html" with notifications=read %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% if notifications %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-condensed table-hover table-striped">
|
|
||||||
<tr>
|
|
||||||
<th class="text-center">{% translate "Timestamp" %}</th>
|
|
||||||
<th class="text-center">{% translate "Title" %}</th>
|
|
||||||
<th class="text-center">{% translate "Action" %}</th>
|
|
||||||
</tr>
|
|
||||||
{% for notif in notifications %}
|
|
||||||
<tr class="{{ notif.level }}">
|
|
||||||
<td class="text-center">{{ notif.timestamp }}</td>
|
|
||||||
<td class="text-center">{{ notif.title }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<a href="{% url 'notifications:view' notif.id %}" class="btn btn-primary" title="View">
|
|
||||||
<span class="glyphicon glyphicon-eye-open"></span>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'notifications:remove' notif.id %}" class="btn btn-danger" title="Remove">
|
|
||||||
<span class="glyphicon glyphicon-remove"></span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-default text-center">{% translate "No notifications." %}</div>
|
|
||||||
{% endif %}
|
|
||||||
@@ -5,22 +5,25 @@
|
|||||||
{% block page_title %}{% translate "View Notification" %}{% endblock page_title %}
|
{% block page_title %}{% translate "View Notification" %}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-header text-center">
|
|
||||||
{% translate "View Notification" %}
|
|
||||||
<div class="text-right">
|
|
||||||
<a href="{% url 'notifications:list' %}" class="btn btn-primary btn-lg">
|
|
||||||
<span class="glyphicon glyphicon-arrow-left"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="col-lg-12">
|
||||||
<div class="col-lg-12">
|
<h1 class="page-header text-center">
|
||||||
<div class="panel panel-{{ notif.level }}">
|
{% translate "View Notification" %}
|
||||||
<div class="panel-heading">{{ notif.timestamp }} {{ notif.title }}</div>
|
<div class="text-right">
|
||||||
<div class="panel-body"><pre>{{ notif.message }}</pre></div>
|
<a href="{% url 'notifications:list' %}" class="btn btn-primary btn-lg">
|
||||||
|
<span class="glyphicon glyphicon-arrow-left"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<div class="col-lg-12 container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="panel panel-{{ notif.level }}">
|
||||||
|
<div class="panel-heading">{{ notif.timestamp }} {{ notif.title }}</div>
|
||||||
|
<div class="panel-body"><pre>{{ notif.message }}</pre></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
|
||||||
|
|
||||||
from ..core import NotifyApiWrapper
|
|
||||||
from ..models import Notification
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserNotificationCount(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
super().setUpClass()
|
|
||||||
cls.user = AuthUtils.create_user("bruce_wayne")
|
|
||||||
|
|
||||||
def test_should_add_danger_notification(self):
|
|
||||||
# given
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
# when
|
|
||||||
notify.danger(user=self.user, title="title", message="message")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, Notification.Level.DANGER)
|
|
||||||
|
|
||||||
def test_should_add_info_notification(self):
|
|
||||||
# given
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
# when
|
|
||||||
notify.info(user=self.user, title="title", message="message")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, Notification.Level.INFO)
|
|
||||||
|
|
||||||
def test_should_add_success_notification(self):
|
|
||||||
# given
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
# when
|
|
||||||
notify.success(user=self.user, title="title", message="message")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, Notification.Level.SUCCESS)
|
|
||||||
|
|
||||||
def test_should_add_warning_notification(self):
|
|
||||||
# given
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
# when
|
|
||||||
notify.warning(user=self.user, title="title", message="message")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, Notification.Level.WARNING)
|
|
||||||
|
|
||||||
def test_should_add_info_notification_via_callable(self):
|
|
||||||
# given
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
# when
|
|
||||||
notify(user=self.user, title="title", message="message")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, Notification.Level.INFO)
|
|
||||||
|
|
||||||
def test_should_add_danger_notification_via_callable(self):
|
|
||||||
# given
|
|
||||||
notify = NotifyApiWrapper()
|
|
||||||
# when
|
|
||||||
notify(user=self.user, title="title", message="message", level="danger")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, Notification.Level.DANGER)
|
|
||||||
@@ -4,8 +4,11 @@ from allianceauth.tests.auth_utils import AuthUtils
|
|||||||
from .. import notify
|
from .. import notify
|
||||||
from ..models import Notification
|
from ..models import Notification
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.notifications'
|
||||||
|
|
||||||
|
|
||||||
class TestUserNotificationCount(TestCase):
|
class TestUserNotificationCount(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.user = AuthUtils.create_user('magic_mike')
|
cls.user = AuthUtils.create_user('magic_mike')
|
||||||
@@ -20,18 +23,6 @@ class TestUserNotificationCount(TestCase):
|
|||||||
alliance_name='RIDERS'
|
alliance_name='RIDERS'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_can_notify_short(self):
|
def test_can_notify(self):
|
||||||
# when
|
notify(self.user, 'dummy')
|
||||||
notify(self.user, "dummy")
|
|
||||||
# then
|
|
||||||
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||||
|
|
||||||
def test_can_notify_full(self):
|
|
||||||
# when
|
|
||||||
notify(user=self.user, title="title", message="message", level="danger")
|
|
||||||
# then
|
|
||||||
obj = Notification.objects.first()
|
|
||||||
self.assertEqual(obj.user, self.user)
|
|
||||||
self.assertEqual(obj.title, "title")
|
|
||||||
self.assertEqual(obj.message, "message")
|
|
||||||
self.assertEqual(obj.level, "danger")
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'notifications'
|
app_name = 'notifications'
|
||||||
# Notifications
|
# Notifications
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^remove_notifications/(\w+)/$', views.remove_notification, name='remove'),
|
path('remove_notifications/<int:notif_id>/', views.remove_notification, name='remove'),
|
||||||
url(r'^notifications/mark_all_read/$', views.mark_all_read, name='mark_all_read'),
|
path('notifications/mark_all_read/', views.mark_all_read, name='mark_all_read'),
|
||||||
url(r'^notifications/delete_all_read/$', views.delete_all_read, name='delete_all_read'),
|
path('notifications/delete_all_read/', views.delete_all_read, name='delete_all_read'),
|
||||||
url(r'^notifications/$', views.notification_list, name='list'),
|
path('notifications/', views.notification_list, name='list'),
|
||||||
url(r'^notifications/(\w+)/$', views.notification_view, name='view'),
|
path('notifications/<int:notif_id>/', views.notification_view, name='view'),
|
||||||
url(
|
path(
|
||||||
r'^user_notifications_count/(?P<user_pk>\d+)/$',
|
'user_notifications_count/<int:user_pk>/',
|
||||||
views.user_notifications_count,
|
views.user_notifications_count,
|
||||||
name='user_notifications_count'
|
name='user_notifications_count'
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from . import urls
|
from . import urls
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from allianceauth.optimer.form_widgets import DataListWidget
|
from allianceauth.optimer.form_widgets import DataListWidget
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<form class="form-signin" role="form" action="" method="POST">
|
<form class="form-signin" role="form" action="" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Create Fleet Operation" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Create Fleet Operation" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<b>{% translate "Current Eve Time:" %} </b>
|
<b>{% translate "Current Eve Time:" %} </b>
|
||||||
</div>
|
</div>
|
||||||
<strong class="label label-info text-left" id="current-time"></strong>
|
<strong class="label label-info text-left" id="current-time"></strong>
|
||||||
<br />
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4><b>{% translate "Next Fleet Operations" %}</b></h4>
|
<h4><b>{% translate "Next Fleet Operations" %}</b></h4>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<form class="form-signin" role="form" action="" method="POST">
|
<form class="form-signin" role="form" action="" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
<br/>
|
<br>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Update Fleet Operation" %}
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Update Fleet Operation" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'optimer'
|
app_name = 'optimer'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.optimer_view, name='view'),
|
path('', views.optimer_view, name='view'),
|
||||||
url(r'^add$', views.add_optimer_view, name='add'),
|
path('add/', views.add_optimer_view, name='add'),
|
||||||
url(r'^(\w+)/remove$', views.remove_optimer, name='remove'),
|
path('<int:optimer_id>/remove/', views.remove_optimer, name='remove'),
|
||||||
url(r'^(\w+)/edit$', views.edit_optimer, name='edit'),
|
path('<int:optimer_id>/edit/', views.edit_optimer, name='edit'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import permission_required
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .form import OpForm
|
from .form import OpForm
|
||||||
|
|
||||||
from .models import OpTimer, OpTimerType
|
from .models import OpTimer, OpTimerType
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'permissions_tool'
|
app_name = 'permissions_tool'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^overview/$', views.permissions_overview, name='overview'),
|
path('overview/', views.permissions_overview, name='overview'),
|
||||||
url(r'^audit/(?P<app_label>[\w\-_]+)/(?P<model>[\w\-_]+)/(?P<codename>[\w\-_]+)/$', views.permissions_audit,
|
re_path(r'^audit/(?P<app_label>[\w\-_]+)/(?P<model>[\w\-_]+)/(?P<codename>[\w\-_]+)/$', views.permissions_audit,
|
||||||
name='audit'),
|
name='audit'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
|
|||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'allianceauth.authentication.middleware.UserSettingsMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@@ -172,11 +173,8 @@ MESSAGE_TAGS = {
|
|||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "redis_cache.RedisCache",
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
"LOCATION": "localhost:6379",
|
"LOCATION": "redis://127.0.0.1:6379/1" # change the 1 here to change the database used
|
||||||
"OPTIONS": {
|
|
||||||
"DB": 1,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ EMAIL_HOST_PASSWORD = ''
|
|||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = True
|
||||||
DEFAULT_FROM_EMAIL = ''
|
DEFAULT_FROM_EMAIL = ''
|
||||||
|
|
||||||
|
# Cache compression can help on bigger auths where ram starts to become an issue.
|
||||||
|
# Uncomment the following 3 lines to enable.
|
||||||
|
|
||||||
|
#CACHES["default"]["OPTIONS"] = {
|
||||||
|
# "COMPRESSOR": "django_redis.compressors.lzma.LzmaCompressor",
|
||||||
|
#}
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
# Add any custom settings below here. #
|
# Add any custom settings below here. #
|
||||||
#######################################
|
#######################################
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include
|
||||||
from allianceauth import urls
|
from allianceauth import urls
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'', include(urls)),
|
re_path(r'', include(urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
handler500 = 'allianceauth.views.Generic500Redirect'
|
handler500 = 'allianceauth.views.Generic500Redirect'
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from allianceauth.authentication.admin import (
|
from allianceauth.authentication.admin import (
|
||||||
MainAllianceFilter,
|
|
||||||
MainCorporationsFilter,
|
|
||||||
user_main_organization,
|
|
||||||
user_profile_pic,
|
user_profile_pic,
|
||||||
user_username,
|
user_username,
|
||||||
|
user_main_organization,
|
||||||
|
MainCorporationsFilter,
|
||||||
|
MainAllianceFilter
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import NameFormatConfig
|
from .models import NameFormatConfig
|
||||||
@@ -36,18 +36,19 @@ class ServicesUserAdmin(admin.ModelAdmin):
|
|||||||
MainAllianceFilter,
|
MainAllianceFilter,
|
||||||
'user__date_joined',
|
'user__date_joined',
|
||||||
)
|
)
|
||||||
list_select_related = (
|
|
||||||
'user', 'user__profile__main_character', 'user__profile__state'
|
|
||||||
)
|
|
||||||
|
|
||||||
@admin.display(ordering='user__profile__state__name')
|
|
||||||
def _state(self, obj):
|
def _state(self, obj):
|
||||||
return obj.user.profile.state.name
|
return obj.user.profile.state.name
|
||||||
|
|
||||||
@admin.display(ordering='user__date_joined')
|
_state.short_description = 'state'
|
||||||
|
_state.admin_order_field = 'user__profile__state__name'
|
||||||
|
|
||||||
def _date_joined(self, obj):
|
def _date_joined(self, obj):
|
||||||
return obj.user.date_joined
|
return obj.user.date_joined
|
||||||
|
|
||||||
|
_date_joined.short_description = 'date joined'
|
||||||
|
_date_joined.admin_order_field = 'user__date_joined'
|
||||||
|
|
||||||
|
|
||||||
class NameFormatConfigForm(forms.ModelForm):
|
class NameFormatConfigForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -61,7 +62,6 @@ class NameFormatConfigForm(forms.ModelForm):
|
|||||||
self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES)
|
self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(NameFormatConfig)
|
|
||||||
class NameFormatConfigAdmin(admin.ModelAdmin):
|
class NameFormatConfigAdmin(admin.ModelAdmin):
|
||||||
form = NameFormatConfigForm
|
form = NameFormatConfigForm
|
||||||
list_display = ('service_name', 'get_state_display_string')
|
list_display = ('service_name', 'get_state_display_string')
|
||||||
@@ -69,3 +69,6 @@ class NameFormatConfigAdmin(admin.ModelAdmin):
|
|||||||
def get_state_display_string(self, obj):
|
def get_state_display_string(self, obj):
|
||||||
return ', '.join([state.name for state in obj.states.all()])
|
return ', '.join([state.name for state in obj.states.all()])
|
||||||
get_state_display_string.short_description = 'States'
|
get_state_display_string.short_description = 'States'
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(NameFormatConfig, NameFormatConfigAdmin)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class FleetFormatterForm(forms.Form):
|
class FleetFormatterForm(forms.Form):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include
|
||||||
|
from django.urls import re_path
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -9,7 +10,6 @@ from allianceauth.hooks import get_hooks
|
|||||||
|
|
||||||
from .models import NameFormatConfig
|
from .models import NameFormatConfig
|
||||||
|
|
||||||
|
|
||||||
def get_extension_logger(name):
|
def get_extension_logger(name):
|
||||||
"""
|
"""
|
||||||
Takes the name of a plugin/extension and generates a child logger of the extensions logger
|
Takes the name of a plugin/extension and generates a child logger of the extensions logger
|
||||||
@@ -157,7 +157,7 @@ class MenuItemHook:
|
|||||||
|
|
||||||
class UrlHook:
|
class UrlHook:
|
||||||
def __init__(self, urls, namespace, base_url):
|
def __init__(self, urls, namespace, base_url):
|
||||||
self.include_pattern = url(base_url, include(urls, namespace=namespace))
|
self.include_pattern = re_path(base_url, include(urls, namespace=namespace))
|
||||||
|
|
||||||
|
|
||||||
class NameFormatter:
|
class NameFormatter:
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import logging
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from ...admin import ServicesUserAdmin
|
|
||||||
from . import __title__
|
from . import __title__
|
||||||
|
from ...admin import ServicesUserAdmin
|
||||||
from .models import DiscordUser
|
from .models import DiscordUser
|
||||||
from .utils import LoggerAddTag
|
from .utils import LoggerAddTag
|
||||||
|
|
||||||
|
|
||||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||||
|
|
||||||
|
|
||||||
@@ -17,16 +18,21 @@ class DiscordUserAdmin(ServicesUserAdmin):
|
|||||||
list_filter = ServicesUserAdmin.list_filter + ('activated',)
|
list_filter = ServicesUserAdmin.list_filter + ('activated',)
|
||||||
ordering = ('-activated',)
|
ordering = ('-activated',)
|
||||||
|
|
||||||
|
def _uid(self, obj):
|
||||||
|
return obj.uid
|
||||||
|
|
||||||
|
_uid.short_description = 'Discord ID (UID)'
|
||||||
|
_uid.admin_order_field = 'uid'
|
||||||
|
|
||||||
|
def _username(self, obj):
|
||||||
|
if obj.username and obj.discriminator:
|
||||||
|
return f'{obj.username}#{obj.discriminator}'
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
def delete_queryset(self, request, queryset):
|
def delete_queryset(self, request, queryset):
|
||||||
for user in queryset:
|
for user in queryset:
|
||||||
user.delete_user()
|
user.delete_user()
|
||||||
|
|
||||||
@admin.display(description='Discord ID (UID)', ordering='uid')
|
_username.short_description = 'Discord Username'
|
||||||
def _uid(self, obj):
|
_username.admin_order_field = 'username'
|
||||||
return obj.uid
|
|
||||||
|
|
||||||
@admin.display(description='Discord Username', ordering='username')
|
|
||||||
def _username(self, obj):
|
|
||||||
if obj.username and obj.discriminator:
|
|
||||||
return f'{obj.username}#{obj.discriminator}'
|
|
||||||
return ''
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
"""Public interface for community apps who want to interact with the Discord server
|
|
||||||
of the current Alliance Auth instance.
|
|
||||||
|
|
||||||
Example
|
|
||||||
=======
|
|
||||||
|
|
||||||
Here is an example for using the api to fetch the current roles from the configured Discord server.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from allianceauth.services.modules.discord.api import create_bot_client, discord_guild_id
|
|
||||||
|
|
||||||
client = create_bot_client() # create a new Discord client
|
|
||||||
guild_id = discord_guild_id() # get the ID of the configured Discord server
|
|
||||||
roles = client.guild_roles(guild_id) # fetch the roles from our Discord server
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
The docs for the client class can be found here: :py:class:`~allianceauth.services.modules.discord.discord_client.client.DiscordClient`
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .app_settings import DISCORD_GUILD_ID
|
|
||||||
from .core import create_bot_client, group_to_role, server_name # noqa
|
|
||||||
from .discord_client.models import Role # noqa
|
|
||||||
from .models import DiscordUser # noqa
|
|
||||||
|
|
||||||
__all__ = ["create_bot_client", "group_to_role", "server_name", "DiscordUser", "Role"]
|
|
||||||
|
|
||||||
|
|
||||||
def discord_guild_id() -> Optional[int]:
|
|
||||||
"""Guild ID of configured Discord server.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Guild ID or ``None`` if not configured
|
|
||||||
"""
|
|
||||||
return int(DISCORD_GUILD_ID) if DISCORD_GUILD_ID else None
|
|
||||||
@@ -2,25 +2,16 @@ from .utils import clean_setting
|
|||||||
|
|
||||||
|
|
||||||
DISCORD_APP_ID = clean_setting('DISCORD_APP_ID', '')
|
DISCORD_APP_ID = clean_setting('DISCORD_APP_ID', '')
|
||||||
"""App ID for the AA bot on Discord. Needs to be set."""
|
|
||||||
|
|
||||||
DISCORD_APP_SECRET = clean_setting('DISCORD_APP_SECRET', '')
|
DISCORD_APP_SECRET = clean_setting('DISCORD_APP_SECRET', '')
|
||||||
"""App secret for the AA bot on Discord. Needs to be set."""
|
|
||||||
|
|
||||||
DISCORD_BOT_TOKEN = clean_setting('DISCORD_BOT_TOKEN', '')
|
DISCORD_BOT_TOKEN = clean_setting('DISCORD_BOT_TOKEN', '')
|
||||||
"""Token used by the AA bot on Discord. Needs to be set."""
|
|
||||||
|
|
||||||
DISCORD_CALLBACK_URL = clean_setting('DISCORD_CALLBACK_URL', '')
|
DISCORD_CALLBACK_URL = clean_setting('DISCORD_CALLBACK_URL', '')
|
||||||
"""Callback URL for OAuth with Discord. Needs to be set."""
|
|
||||||
|
|
||||||
DISCORD_GUILD_ID = clean_setting('DISCORD_GUILD_ID', '')
|
DISCORD_GUILD_ID = clean_setting('DISCORD_GUILD_ID', '')
|
||||||
"""ID of the Discord Server. Needs to be set."""
|
|
||||||
|
|
||||||
|
# max retries of tasks after an error occurred
|
||||||
DISCORD_TASKS_MAX_RETRIES = clean_setting('DISCORD_TASKS_MAX_RETRIES', 3)
|
DISCORD_TASKS_MAX_RETRIES = clean_setting('DISCORD_TASKS_MAX_RETRIES', 3)
|
||||||
"""Max retries of tasks after an error occurred."""
|
|
||||||
|
|
||||||
|
# Pause in seconds until next retry for tasks after the API returned an error
|
||||||
DISCORD_TASKS_RETRY_PAUSE = clean_setting('DISCORD_TASKS_RETRY_PAUSE', 60)
|
DISCORD_TASKS_RETRY_PAUSE = clean_setting('DISCORD_TASKS_RETRY_PAUSE', 60)
|
||||||
"""Pause in seconds until next retry for tasks after the API returned an error."""
|
|
||||||
|
|
||||||
|
# automatically sync Discord users names to user's main character name when created
|
||||||
DISCORD_SYNC_NAMES = clean_setting('DISCORD_SYNC_NAMES', False)
|
DISCORD_SYNC_NAMES = clean_setting('DISCORD_SYNC_NAMES', False)
|
||||||
"""Automatically sync Discord users names to user's main character name when created."""
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.template.loader import render_to_string
|
|||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from allianceauth.services.hooks import ServicesHook
|
from allianceauth.services.hooks import ServicesHook
|
||||||
|
|
||||||
from .core import server_name, user_formatted_nick
|
|
||||||
from .models import DiscordUser
|
from .models import DiscordUser
|
||||||
from .urls import urlpatterns
|
from .urls import urlpatterns
|
||||||
from .utils import LoggerAddTag
|
from .utils import LoggerAddTag
|
||||||
@@ -54,7 +53,7 @@ class DiscordService(ServicesHook):
|
|||||||
return render_to_string(
|
return render_to_string(
|
||||||
self.service_ctrl_template,
|
self.service_ctrl_template,
|
||||||
{
|
{
|
||||||
'server_name': server_name(),
|
'server_name': DiscordUser.objects.server_name(),
|
||||||
'user_has_account': user_has_account,
|
'user_has_account': user_has_account,
|
||||||
'discord_username': discord_username
|
'discord_username': discord_username
|
||||||
},
|
},
|
||||||
@@ -74,7 +73,7 @@ class DiscordService(ServicesHook):
|
|||||||
'user_pk': user.pk,
|
'user_pk': user.pk,
|
||||||
# since the new nickname is not yet in the DB we need to
|
# since the new nickname is not yet in the DB we need to
|
||||||
# provide it manually to the task
|
# provide it manually to the task
|
||||||
'nickname': user_formatted_nick(user)
|
'nickname': DiscordUser.objects.user_formatted_nick(user)
|
||||||
},
|
},
|
||||||
priority=SINGLE_TASK_PRIORITY
|
priority=SINGLE_TASK_PRIORITY
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
"""Core functionality of the Discord service not directly related to models."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
from requests.exceptions import HTTPError
|
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, User
|
|
||||||
|
|
||||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
|
||||||
from allianceauth.services.hooks import NameFormatter
|
|
||||||
|
|
||||||
from . import __title__
|
|
||||||
from .app_settings import DISCORD_BOT_TOKEN, DISCORD_GUILD_ID
|
|
||||||
from .discord_client import DiscordClient, RolesSet, Role
|
|
||||||
from .discord_client.exceptions import DiscordClientException
|
|
||||||
from .utils import LoggerAddTag
|
|
||||||
|
|
||||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
|
||||||
|
|
||||||
|
|
||||||
def create_bot_client(is_rate_limited: bool = True) -> DiscordClient:
|
|
||||||
"""Create new bot client for accessing the configured Discord server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
is_rate_limited: Set to False to turn off rate limiting (use with care).
|
|
||||||
|
|
||||||
Return:
|
|
||||||
Discord client instance
|
|
||||||
"""
|
|
||||||
return DiscordClient(DISCORD_BOT_TOKEN, is_rate_limited=is_rate_limited)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_roles_for_user(
|
|
||||||
user: User,
|
|
||||||
client: DiscordClient,
|
|
||||||
discord_uid: int,
|
|
||||||
state_name: str = None,
|
|
||||||
) -> Tuple[RolesSet, Optional[bool]]:
|
|
||||||
"""Calculate current Discord roles for an Auth user.
|
|
||||||
|
|
||||||
Takes into account reserved groups and existing managed roles (e.g. nitro).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- Discord roles, changed flag:
|
|
||||||
- True when roles have changed,
|
|
||||||
- False when they have not changed,
|
|
||||||
- None if user is not a member of the guild
|
|
||||||
"""
|
|
||||||
roles_calculated = client.match_or_create_roles_from_names_2(
|
|
||||||
guild_id=DISCORD_GUILD_ID,
|
|
||||||
role_names=_user_group_names(user=user, state_name=state_name),
|
|
||||||
)
|
|
||||||
logger.debug("Calculated roles for user %s: %s", user, roles_calculated.ids())
|
|
||||||
roles_current = client.guild_member_roles(
|
|
||||||
guild_id=DISCORD_GUILD_ID, user_id=discord_uid
|
|
||||||
)
|
|
||||||
if roles_current is None:
|
|
||||||
logger.debug("User %s is not a member of the guild.", user)
|
|
||||||
return roles_calculated, None
|
|
||||||
logger.debug("Current roles user %s: %s", user, roles_current.ids())
|
|
||||||
reserved_role_names = ReservedGroupName.objects.values_list("name", flat=True)
|
|
||||||
roles_reserved = roles_current.subset(role_names=reserved_role_names)
|
|
||||||
roles_managed = roles_current.subset(managed_only=True)
|
|
||||||
roles_persistent = roles_managed.union(roles_reserved)
|
|
||||||
if roles_calculated == roles_current.difference(roles_persistent):
|
|
||||||
return roles_calculated, False
|
|
||||||
return roles_calculated.union(roles_persistent), True
|
|
||||||
|
|
||||||
|
|
||||||
def _user_group_names(user: User, state_name: str = None) -> List[str]:
|
|
||||||
"""Names of groups and state the given user is a member of."""
|
|
||||||
if not state_name:
|
|
||||||
state_name = user.profile.state.name
|
|
||||||
group_names = [group.name for group in user.groups.all()] + [state_name]
|
|
||||||
logger.debug("Group names for roles updates of user %s are: %s", user, group_names)
|
|
||||||
return group_names
|
|
||||||
|
|
||||||
|
|
||||||
def user_formatted_nick(user: User) -> Optional[str]:
|
|
||||||
"""Name of the given user's main character with name formatting applied.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Name or ``None`` if user has no main.
|
|
||||||
"""
|
|
||||||
from .auth_hooks import DiscordService
|
|
||||||
|
|
||||||
if user.profile.main_character:
|
|
||||||
return NameFormatter(DiscordService(), user).format_name()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def group_to_role(group: Group) -> Optional[Role]:
|
|
||||||
"""Fetch the Discord role matching the given Django group by name.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Discord role or None if no matching role exist
|
|
||||||
"""
|
|
||||||
return default_bot_client.match_role_from_name(
|
|
||||||
guild_id=DISCORD_GUILD_ID, role_name=group.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def server_name(use_cache: bool = True) -> str:
|
|
||||||
"""Fetches the name of the current Discord server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
use_cache: When set False will force an API call to get the server name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Server name or an empty string if the name could not be retrieved
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
server_name = default_bot_client.guild_name(
|
|
||||||
guild_id=DISCORD_GUILD_ID, use_cache=use_cache
|
|
||||||
)
|
|
||||||
except (HTTPError, DiscordClientException):
|
|
||||||
server_name = ""
|
|
||||||
except Exception:
|
|
||||||
logger.warning(
|
|
||||||
"Unexpected error when trying to retrieve the server name from Discord",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
server_name = ""
|
|
||||||
return server_name
|
|
||||||
|
|
||||||
|
|
||||||
# Default bot client to be used by modules of this package
|
|
||||||
default_bot_client = create_bot_client()
|
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
from .app_settings import DISCORD_OAUTH_BASE_URL, DISCORD_OAUTH_TOKEN_URL # noqa
|
from .client import DiscordClient # noqa
|
||||||
from .client import DiscordClient # noqa
|
from .exceptions import DiscordApiBackoff # noqa
|
||||||
from .exceptions import ( # noqa
|
from .helpers import DiscordRoles # noqa
|
||||||
DiscordApiBackoff,
|
|
||||||
DiscordClientException,
|
|
||||||
DiscordRateLimitExhausted,
|
|
||||||
DiscordTooManyRequestsError,
|
|
||||||
)
|
|
||||||
from .helpers import RolesSet # noqa
|
|
||||||
from .models import Guild, GuildMember, Role, User # noqa
|
|
||||||
|
|||||||
@@ -1,56 +1,45 @@
|
|||||||
"""Settings for the Discord client.
|
|
||||||
|
|
||||||
To overwrite a default set the variable in your local Django settings, e.g:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
DISCORD_GUILD_NAME_CACHE_MAX_AGE = 7200
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ..utils import clean_setting
|
from ..utils import clean_setting
|
||||||
|
|
||||||
|
|
||||||
|
# Base URL for all API calls. Must end with /.
|
||||||
DISCORD_API_BASE_URL = clean_setting(
|
DISCORD_API_BASE_URL = clean_setting(
|
||||||
'DISCORD_API_BASE_URL', 'https://discord.com/api/'
|
'DISCORD_API_BASE_URL', 'https://discord.com/api/'
|
||||||
)
|
)
|
||||||
"""Base URL for all API calls. Must end with /."""
|
|
||||||
|
|
||||||
|
# Low level connecttimeout for requests to the Discord API in seconds
|
||||||
DISCORD_API_TIMEOUT_CONNECT = clean_setting(
|
DISCORD_API_TIMEOUT_CONNECT = clean_setting(
|
||||||
'DISCORD_API_TIMEOUT', 5
|
'DISCORD_API_TIMEOUT', 5
|
||||||
)
|
)
|
||||||
"""Low level connect timeout for requests to the Discord API in seconds."""
|
|
||||||
|
|
||||||
|
# Low level read timeout for requests to the Discord API in seconds
|
||||||
DISCORD_API_TIMEOUT_READ = clean_setting(
|
DISCORD_API_TIMEOUT_READ = clean_setting(
|
||||||
'DISCORD_API_TIMEOUT', 30
|
'DISCORD_API_TIMEOUT', 30
|
||||||
)
|
)
|
||||||
"""Low level read timeout for requests to the Discord API in seconds."""
|
|
||||||
|
|
||||||
|
# Base authorization URL for Discord Oauth
|
||||||
DISCORD_OAUTH_BASE_URL = clean_setting(
|
DISCORD_OAUTH_BASE_URL = clean_setting(
|
||||||
'DISCORD_OAUTH_BASE_URL', 'https://discord.com/api/oauth2/authorize'
|
'DISCORD_OAUTH_BASE_URL', 'https://discord.com/api/oauth2/authorize'
|
||||||
)
|
)
|
||||||
"""Base authorization URL for Discord Oauth."""
|
|
||||||
|
|
||||||
|
# Base authorization URL for Discord Oauth
|
||||||
DISCORD_OAUTH_TOKEN_URL = clean_setting(
|
DISCORD_OAUTH_TOKEN_URL = clean_setting(
|
||||||
'DISCORD_OAUTH_TOKEN_URL', 'https://discord.com/api/oauth2/token'
|
'DISCORD_OAUTH_TOKEN_URL', 'https://discord.com/api/oauth2/token'
|
||||||
)
|
)
|
||||||
"""Base authorization URL for Discord Oauth."""
|
|
||||||
|
|
||||||
|
# How long the Discord guild names retrieved from the server are
|
||||||
|
# caches locally in seconds.
|
||||||
DISCORD_GUILD_NAME_CACHE_MAX_AGE = clean_setting(
|
DISCORD_GUILD_NAME_CACHE_MAX_AGE = clean_setting(
|
||||||
'DISCORD_GUILD_NAME_CACHE_MAX_AGE', 3600 * 24
|
'DISCORD_GUILD_NAME_CACHE_MAX_AGE', 3600 * 24
|
||||||
)
|
)
|
||||||
"""How long the Discord guild names retrieved from the server
|
|
||||||
are caches locally in seconds.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
# How long Discord roles retrieved from the server are caches locally in seconds.
|
||||||
DISCORD_ROLES_CACHE_MAX_AGE = clean_setting(
|
DISCORD_ROLES_CACHE_MAX_AGE = clean_setting(
|
||||||
'DISCORD_ROLES_CACHE_MAX_AGE', 3600 * 1
|
'DISCORD_ROLES_CACHE_MAX_AGE', 3600 * 1
|
||||||
)
|
)
|
||||||
"""How long Discord roles retrieved from the server are caches locally in seconds."""
|
|
||||||
|
|
||||||
|
# Turns off creation of new roles. In case the rate limit for creating roles is
|
||||||
|
# exhausted, this setting allows the Discord service to continue to function
|
||||||
|
# and wait out the reset. Rate limit is about 250 per 48 hrs.
|
||||||
DISCORD_DISABLE_ROLE_CREATION = clean_setting(
|
DISCORD_DISABLE_ROLE_CREATION = clean_setting(
|
||||||
'DISCORD_DISABLE_ROLE_CREATION', False
|
'DISCORD_DISABLE_ROLE_CREATION', False
|
||||||
)
|
)
|
||||||
"""Turns off creation of new roles. In case the rate limit for creating roles is
|
|
||||||
exhausted, this setting allows the Discord service to continue to function
|
|
||||||
and wait out the reset. Rate limit is about 250 per 48 hrs.
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
"""Client for interacting with the Discord API."""
|
from hashlib import md5
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from enum import IntEnum
|
|
||||||
from hashlib import md5
|
|
||||||
from http import HTTPStatus
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Iterable, List, Optional, Set, Tuple
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from uuid import uuid1
|
from uuid import uuid1
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests.exceptions import HTTPError
|
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
import requests
|
||||||
|
|
||||||
from allianceauth.utils.cache import get_redis_client
|
from django_redis import get_redis_connection
|
||||||
|
|
||||||
from allianceauth import __title__ as AUTH_TITLE
|
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
|
||||||
from allianceauth import __url__, __version__
|
|
||||||
|
|
||||||
from .. import __title__
|
from .. import __title__
|
||||||
from ..utils import LoggerAddTag
|
|
||||||
from .app_settings import (
|
from .app_settings import (
|
||||||
DISCORD_API_BASE_URL,
|
DISCORD_API_BASE_URL,
|
||||||
DISCORD_API_TIMEOUT_CONNECT,
|
DISCORD_API_TIMEOUT_CONNECT,
|
||||||
DISCORD_API_TIMEOUT_READ,
|
DISCORD_API_TIMEOUT_READ,
|
||||||
DISCORD_DISABLE_ROLE_CREATION,
|
DISCORD_DISABLE_ROLE_CREATION,
|
||||||
DISCORD_GUILD_NAME_CACHE_MAX_AGE,
|
DISCORD_GUILD_NAME_CACHE_MAX_AGE,
|
||||||
|
DISCORD_OAUTH_BASE_URL,
|
||||||
|
DISCORD_OAUTH_TOKEN_URL,
|
||||||
DISCORD_ROLES_CACHE_MAX_AGE,
|
DISCORD_ROLES_CACHE_MAX_AGE,
|
||||||
)
|
)
|
||||||
from .exceptions import DiscordRateLimitExhausted, DiscordTooManyRequestsError
|
from .exceptions import DiscordRateLimitExhausted, DiscordTooManyRequestsError
|
||||||
from .helpers import RolesSet
|
from .helpers import DiscordRoles
|
||||||
from .models import Guild, GuildMember, Role, User
|
from ..utils import LoggerAddTag
|
||||||
|
|
||||||
|
|
||||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||||
|
|
||||||
@@ -63,13 +58,8 @@ MINIMUM_BLOCKING_WAIT = 50
|
|||||||
RATE_LIMIT_RETRIES = 1000
|
RATE_LIMIT_RETRIES = 1000
|
||||||
|
|
||||||
|
|
||||||
class DiscordApiStatusCode(IntEnum):
|
|
||||||
"""Status code returned from the Discord API."""
|
|
||||||
UNKNOWN_MEMBER = 10007 #:
|
|
||||||
|
|
||||||
|
|
||||||
class DiscordClient:
|
class DiscordClient:
|
||||||
"""This class provides a web client for interacting with the Discord API.
|
"""This class provides a web client for interacting with the Discord API
|
||||||
|
|
||||||
The client has rate limiting that supports concurrency.
|
The client has rate limiting that supports concurrency.
|
||||||
This means it is able to ensure the API rate limit is not violated,
|
This means it is able to ensure the API rate limit is not violated,
|
||||||
@@ -77,30 +67,24 @@ class DiscordClient:
|
|||||||
|
|
||||||
In addition the client support proper API backoff.
|
In addition the client support proper API backoff.
|
||||||
|
|
||||||
Synchronization of rate limit infos across multiple processes
|
Synchronization of rate limit infos accross multiple processes
|
||||||
is implemented with Redis and thus requires Redis as Django cache backend.
|
is implemented with Redis and thus requires Redis as Django cache backend.
|
||||||
|
|
||||||
The cache is shared across all clients and processes (also using Redis).
|
|
||||||
|
|
||||||
All durations are in milliseconds.
|
All durations are in milliseconds.
|
||||||
|
|
||||||
Most errors from the API will raise a requests.HTTPError.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
access_token: Discord access token used to authenticate all calls to the API
|
|
||||||
redis: Redis instance to be used.
|
|
||||||
is_rate_limited: Set to False to turn off rate limiting (use with care).
|
|
||||||
If not specified will try to use the Redis instance
|
|
||||||
from the default Django cache backend.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: No access token provided
|
|
||||||
"""
|
"""
|
||||||
|
OAUTH_BASE_URL = DISCORD_OAUTH_BASE_URL
|
||||||
|
OAUTH_TOKEN_URL = DISCORD_OAUTH_TOKEN_URL
|
||||||
|
|
||||||
_KEY_GLOBAL_BACKOFF_UNTIL = 'DISCORD_GLOBAL_BACKOFF_UNTIL'
|
_KEY_GLOBAL_BACKOFF_UNTIL = 'DISCORD_GLOBAL_BACKOFF_UNTIL'
|
||||||
_KEY_GLOBAL_RATE_LIMIT_REMAINING = 'DISCORD_GLOBAL_RATE_LIMIT_REMAINING'
|
_KEY_GLOBAL_RATE_LIMIT_REMAINING = 'DISCORD_GLOBAL_RATE_LIMIT_REMAINING'
|
||||||
_KEYPREFIX_GUILD_NAME = 'DISCORD_GUILD_NAME'
|
_KEYPREFIX_GUILD_NAME = 'DISCORD_GUILD_NAME'
|
||||||
_KEYPREFIX_GUILD_ROLES = 'DISCORD_GUILD_ROLES'
|
_KEYPREFIX_GUILD_ROLES = 'DISCORD_GUILD_ROLES'
|
||||||
_KEYPREFIX_ROLE_NAME = 'DISCORD_ROLE_NAME'
|
_KEYPREFIX_ROLE_NAME = 'DISCORD_ROLE_NAME'
|
||||||
|
_NICK_MAX_CHARS = 32
|
||||||
|
|
||||||
|
_HTTP_STATUS_CODE_NOT_FOUND = 404
|
||||||
|
_HTTP_STATUS_CODE_RATE_LIMITED = 429
|
||||||
|
_DISCORD_STATUS_CODE_UNKNOWN_MEMBER = 10007
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -108,12 +92,18 @@ class DiscordClient:
|
|||||||
redis: Redis = None,
|
redis: Redis = None,
|
||||||
is_rate_limited: bool = True
|
is_rate_limited: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
if not access_token:
|
"""
|
||||||
raise ValueError('You must provide an access token.')
|
Params:
|
||||||
|
- access_token: Discord access token used to authenticate all calls to the API
|
||||||
|
- redis: Redis instance to be used.
|
||||||
|
- is_rate_limited: Set to False to run of rate limiting (use with care)
|
||||||
|
If not specified will try to use the Redis instance
|
||||||
|
from the default Django cache backend.
|
||||||
|
"""
|
||||||
self._access_token = str(access_token)
|
self._access_token = str(access_token)
|
||||||
self._is_rate_limited = bool(is_rate_limited)
|
self._is_rate_limited = bool(is_rate_limited)
|
||||||
if not redis:
|
if not redis:
|
||||||
self._redis = get_redis_client()
|
self._redis = get_redis_connection("default")
|
||||||
if not isinstance(self._redis, Redis):
|
if not isinstance(self._redis, Redis):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'This class requires a Redis client, but none was provided '
|
'This class requires a Redis client, but none was provided '
|
||||||
@@ -141,20 +131,19 @@ class DiscordClient:
|
|||||||
self.__redis_script_set_longer = self._redis.register_script(lua_2)
|
self.__redis_script_set_longer = self._redis.register_script(lua_2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_token(self) -> str:
|
def access_token(self):
|
||||||
"""Discord access token."""
|
|
||||||
return self._access_token
|
return self._access_token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_rate_limited(self) -> bool:
|
def is_rate_limited(self):
|
||||||
"""Wether this instance is rate limited."""
|
|
||||||
return self._is_rate_limited
|
return self._is_rate_limited
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'{type(self).__name__}(access_token=...{self.access_token[-5:]})'
|
return f'{type(self).__name__}(access_token=...{self.access_token[-5:]})'
|
||||||
|
|
||||||
def _redis_decr_or_set(self, name: str, value: str, px: int) -> bool:
|
def _redis_decr_or_set(self, name: str, value: str, px: int) -> bool:
|
||||||
"""Decrease the key value if it exists and returns the result else set the key.
|
"""decreases the key value if it exists and returns the result
|
||||||
|
else sets the key
|
||||||
|
|
||||||
Implemented as Lua script to ensure atomicity.
|
Implemented as Lua script to ensure atomicity.
|
||||||
"""
|
"""
|
||||||
@@ -163,7 +152,7 @@ class DiscordClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _redis_set_if_longer(self, name: str, value: str, px: int) -> bool:
|
def _redis_set_if_longer(self, name: str, value: str, px: int) -> bool:
|
||||||
"""Like set, but only goes through if either key doesn't exist
|
"""like set, but only goes through if either key doesn't exist
|
||||||
or px would be extended.
|
or px would be extended.
|
||||||
|
|
||||||
Implemented as Lua script to ensure atomicity.
|
Implemented as Lua script to ensure atomicity.
|
||||||
@@ -174,134 +163,111 @@ class DiscordClient:
|
|||||||
|
|
||||||
# users
|
# users
|
||||||
|
|
||||||
def current_user(self) -> User:
|
def current_user(self) -> dict:
|
||||||
"""Fetch user belonging to the current access_token."""
|
"""returns the user belonging to the current access_token"""
|
||||||
authorization = f'Bearer {self.access_token}'
|
authorization = f'Bearer {self.access_token}'
|
||||||
r = self._api_request(
|
r = self._api_request(
|
||||||
method='get', route='users/@me', authorization=authorization
|
method='get', route='users/@me', authorization=authorization
|
||||||
)
|
)
|
||||||
return User.from_dict(r.json())
|
return r.json()
|
||||||
|
|
||||||
# guild
|
# guild
|
||||||
|
|
||||||
def guild_infos(self, guild_id: int) -> Guild:
|
def guild_infos(self, guild_id: int) -> dict:
|
||||||
"""Fetch all basic infos about this guild.
|
"""Returns all basic infos about this guild"""
|
||||||
|
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
"""
|
|
||||||
route = f"guilds/{guild_id}"
|
route = f"guilds/{guild_id}"
|
||||||
r = self._api_request(method='get', route=route)
|
r = self._api_request(method='get', route=route)
|
||||||
return Guild.from_dict(r.json())
|
return r.json()
|
||||||
|
|
||||||
def guild_name(self, guild_id: int, use_cache: bool = True) -> str:
|
def guild_name(self, guild_id: int, use_cache: bool = True) -> str:
|
||||||
"""Fetch the name of this guild (cached).
|
"""returns the name of this guild (cached)
|
||||||
|
or an empty string if something went wrong
|
||||||
|
|
||||||
Args:
|
Params:
|
||||||
guild_id: Discord ID of the guild
|
- guild_id: ID of current guild
|
||||||
use_cache: When set to False will force an API call to get the server name
|
- use_cache: When set to False will force an API call to get the server name
|
||||||
|
|
||||||
Returns:
|
|
||||||
Name of the server or an empty string if something went wrong.
|
|
||||||
"""
|
"""
|
||||||
key_name = self._guild_name_cache_key(guild_id)
|
key_name = self._guild_name_cache_key(guild_id)
|
||||||
if use_cache:
|
if use_cache:
|
||||||
guild_name = self._redis_decode(self._redis.get(key_name))
|
guild_name = self._redis_decode(self._redis.get(key_name))
|
||||||
else:
|
else:
|
||||||
guild_name = ""
|
guild_name = None
|
||||||
if not guild_name:
|
if not guild_name:
|
||||||
try:
|
guild_infos = self.guild_infos(guild_id)
|
||||||
guild = self.guild_infos(guild_id)
|
if 'name' in guild_infos:
|
||||||
except HTTPError:
|
guild_name = guild_infos['name']
|
||||||
guild_name = ""
|
|
||||||
else:
|
|
||||||
guild_name = guild.name
|
|
||||||
self._redis.set(
|
self._redis.set(
|
||||||
name=key_name, value=guild_name, ex=DISCORD_GUILD_NAME_CACHE_MAX_AGE
|
name=key_name,
|
||||||
|
value=guild_name,
|
||||||
|
ex=DISCORD_GUILD_NAME_CACHE_MAX_AGE
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
guild_name = ''
|
||||||
|
|
||||||
return guild_name
|
return guild_name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _guild_name_cache_key(cls, guild_id: int) -> str:
|
def _guild_name_cache_key(cls, guild_id: int) -> str:
|
||||||
"""Construct key for accessing role given by name in the role cache.
|
"""Returns key for accessing role given by name in the role cache"""
|
||||||
|
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
"""
|
|
||||||
gen_key = DiscordClient._generate_hash(f'{guild_id}')
|
gen_key = DiscordClient._generate_hash(f'{guild_id}')
|
||||||
return f'{cls._KEYPREFIX_GUILD_NAME}__{gen_key}'
|
return f'{cls._KEYPREFIX_GUILD_NAME}__{gen_key}'
|
||||||
|
|
||||||
# guild roles
|
# guild roles
|
||||||
|
|
||||||
def guild_roles(self, guild_id: int, use_cache: bool = True) -> Set[Role]:
|
def guild_roles(self, guild_id: int, use_cache: bool = True) -> list:
|
||||||
"""Fetch all roles for this guild.
|
"""Returns the list of all roles for this guild
|
||||||
|
|
||||||
Args:
|
If use_cache is set to False it will always hit the API to retrieve
|
||||||
guild_id: Discord ID of the guild
|
fresh data and update the cache
|
||||||
use_cache: If is set to False it will always hit the API to retrieve
|
|
||||||
fresh data and update the cache.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
"""
|
"""
|
||||||
cache_key = self._guild_roles_cache_key(guild_id)
|
cache_key = self._guild_roles_cache_key(guild_id)
|
||||||
roles = None
|
|
||||||
if use_cache:
|
if use_cache:
|
||||||
roles_raw = self._redis.get(name=cache_key)
|
roles_raw = self._redis.get(name=cache_key)
|
||||||
if roles_raw:
|
if roles_raw:
|
||||||
logger.debug('Returning roles for guild %s from cache', guild_id)
|
logger.debug('Returning roles for guild %s from cache', guild_id)
|
||||||
roles = json.loads(self._redis_decode(roles_raw))
|
return json.loads(self._redis_decode(roles_raw))
|
||||||
logger.debug('No roles for guild %s in cache', guild_id)
|
else:
|
||||||
if roles is None:
|
logger.debug('No roles for guild %s in cache', guild_id)
|
||||||
route = f"guilds/{guild_id}/roles"
|
|
||||||
r = self._api_request(method='get', route=route)
|
route = f"guilds/{guild_id}/roles"
|
||||||
roles = r.json()
|
r = self._api_request(method='get', route=route)
|
||||||
if not roles or not isinstance(roles, list):
|
roles = r.json()
|
||||||
raise RuntimeError(
|
if roles and isinstance(roles, list):
|
||||||
f"Unexpected response when fetching roles from API: {roles}"
|
|
||||||
)
|
|
||||||
self._redis.set(
|
self._redis.set(
|
||||||
name=cache_key,
|
name=cache_key,
|
||||||
value=json.dumps(roles),
|
value=json.dumps(roles),
|
||||||
ex=DISCORD_ROLES_CACHE_MAX_AGE
|
ex=DISCORD_ROLES_CACHE_MAX_AGE
|
||||||
)
|
)
|
||||||
return {Role.from_dict(role) for role in roles}
|
return roles
|
||||||
|
|
||||||
def create_guild_role(
|
def create_guild_role(self, guild_id: int, role_name: str, **kwargs) -> dict:
|
||||||
self, guild_id: int, role_name: str, **kwargs
|
|
||||||
) -> Optional[Role]:
|
|
||||||
"""Create a new guild role with the given name.
|
"""Create a new guild role with the given name.
|
||||||
|
|
||||||
See official documentation for additional optional parameters.
|
See official documentation for additional optional parameters.
|
||||||
|
|
||||||
Note that Discord allows the creation of multiple roles with the same name,
|
Note that Discord allows the creation of multiple roles with the same name,
|
||||||
so to avoid duplicates it's important to check existing roles
|
so to avoid duplicates it's important to check existing roles
|
||||||
before creating new one
|
before creating new one
|
||||||
|
|
||||||
Args:
|
returns a new role dict on success
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
role_name: Name of new role to create
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
new role on success
|
|
||||||
"""
|
"""
|
||||||
route = f"guilds/{guild_id}/roles"
|
route = f"guilds/{guild_id}/roles"
|
||||||
data = {'name': Role.sanitize_name(role_name)}
|
data = {'name': DiscordRoles.sanitize_role_name(role_name)}
|
||||||
data.update(kwargs)
|
data.update(kwargs)
|
||||||
r = self._api_request(method='post', route=route, data=data)
|
r = self._api_request(method='post', route=route, data=data)
|
||||||
role = r.json()
|
role = r.json()
|
||||||
if role:
|
if role:
|
||||||
self._invalidate_guild_roles_cache(guild_id)
|
self._invalidate_guild_roles_cache(guild_id)
|
||||||
return Role.from_dict(role)
|
return role
|
||||||
return None
|
|
||||||
|
|
||||||
def delete_guild_role(self, guild_id: int, role_id: int) -> bool:
|
def delete_guild_role(self, guild_id: int, role_id: int) -> bool:
|
||||||
"""Delete a guild role."""
|
"""Deletes a guild role"""
|
||||||
route = f"guilds/{guild_id}/roles/{role_id}"
|
route = f"guilds/{guild_id}/roles/{role_id}"
|
||||||
r = self._api_request(method='delete', route=route)
|
r = self._api_request(method='delete', route=route)
|
||||||
if r.status_code == 204:
|
if r.status_code == 204:
|
||||||
self._invalidate_guild_roles_cache(guild_id)
|
self._invalidate_guild_roles_cache(guild_id)
|
||||||
return True
|
return True
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def _invalidate_guild_roles_cache(self, guild_id: int) -> None:
|
def _invalidate_guild_roles_cache(self, guild_id: int) -> None:
|
||||||
cache_key = self._guild_roles_cache_key(guild_id)
|
cache_key = self._guild_roles_cache_key(guild_id)
|
||||||
@@ -310,79 +276,67 @@ class DiscordClient:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _guild_roles_cache_key(cls, guild_id: int) -> str:
|
def _guild_roles_cache_key(cls, guild_id: int) -> str:
|
||||||
"""Construct key for accessing cached roles for a guild.
|
"""Returns key for accessing cached roles for a guild"""
|
||||||
|
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
"""
|
|
||||||
gen_key = cls._generate_hash(f'{guild_id}')
|
gen_key = cls._generate_hash(f'{guild_id}')
|
||||||
return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}'
|
return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}'
|
||||||
|
|
||||||
def match_role_from_name(self, guild_id: int, role_name: str) -> Optional[Role]:
|
def match_role_from_name(self, guild_id: int, role_name: str) -> dict:
|
||||||
"""Fetch Discord role matching the given name (cached).
|
"""returns Discord role matching the given name or an empty dict"""
|
||||||
|
guild_roles = DiscordRoles(self.guild_roles(guild_id))
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
role_name: Name of role
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Matching role or None if no match is found
|
|
||||||
"""
|
|
||||||
guild_roles = RolesSet(self.guild_roles(guild_id))
|
|
||||||
return guild_roles.role_by_name(role_name)
|
return guild_roles.role_by_name(role_name)
|
||||||
|
|
||||||
def match_or_create_roles_from_names(
|
def match_or_create_roles_from_names(self, guild_id: int, role_names: list) -> list:
|
||||||
self, guild_id: int, role_names: Iterable[str]
|
"""returns Discord roles matching the given names
|
||||||
) -> List[Tuple[Role, bool]]:
|
|
||||||
"""Fetch or create Discord roles matching the given names (cached).
|
Returns as list of tuple of role and created flag
|
||||||
|
|
||||||
Will try to match with existing roles names
|
Will try to match with existing roles names
|
||||||
Non-existing roles will be created, then created flag will be True
|
Non-existing roles will be created, then created flag will be True
|
||||||
|
|
||||||
Args:
|
Params:
|
||||||
guild_id: ID of guild
|
- guild_id: ID of guild
|
||||||
role_names: list of name strings each defining a role
|
- role_names: list of name strings each defining a role
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of tuple of Role and created flag
|
|
||||||
"""
|
"""
|
||||||
roles = list()
|
roles = list()
|
||||||
guild_roles = RolesSet(self.guild_roles(guild_id))
|
guild_roles = DiscordRoles(self.guild_roles(guild_id))
|
||||||
role_names_cleaned = {Role.sanitize_name(name) for name in role_names}
|
role_names_cleaned = {
|
||||||
|
DiscordRoles.sanitize_role_name(name) for name in role_names
|
||||||
|
}
|
||||||
for role_name in role_names_cleaned:
|
for role_name in role_names_cleaned:
|
||||||
role, created = self.match_or_create_role_from_name(
|
role, created = self.match_or_create_role_from_name(
|
||||||
guild_id=guild_id, role_name=role_name, guild_roles=guild_roles
|
guild_id=guild_id,
|
||||||
|
role_name=DiscordRoles.sanitize_role_name(role_name),
|
||||||
|
guild_roles=guild_roles
|
||||||
)
|
)
|
||||||
if role:
|
if role:
|
||||||
roles.append((role, created))
|
roles.append((role, created))
|
||||||
if created:
|
if created:
|
||||||
guild_roles = guild_roles.union(RolesSet([role]))
|
guild_roles = guild_roles.union(DiscordRoles([role]))
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
def match_or_create_role_from_name(
|
def match_or_create_role_from_name(
|
||||||
self, guild_id: int, role_name: str, guild_roles: RolesSet = None
|
self, guild_id: int, role_name: str, guild_roles: DiscordRoles = None
|
||||||
) -> Tuple[Role, bool]:
|
) -> tuple:
|
||||||
"""Fetch or create Discord role matching the given name.
|
"""returns Discord role matching the given name
|
||||||
|
|
||||||
|
Returns as tuple of role and created flag
|
||||||
|
|
||||||
Will try to match with existing roles names
|
Will try to match with existing roles names
|
||||||
Non-existing roles will be created, then created flag will be True
|
Non-existing roles will be created, then created flag will be True
|
||||||
|
|
||||||
Args:
|
Params:
|
||||||
guild_id: ID of guild
|
- guild_id: ID of guild
|
||||||
role_name: strings defining name of a role
|
- role_name: strings defining name of a role
|
||||||
guild_roles: All known guild roles as RolesSet object.
|
- guild_roles: All known guild roles as DiscordRoles object.
|
||||||
Helps to void redundant lookups of guild roles
|
Helps to void redundant lookups of guild roles
|
||||||
when this method is used multiple times.
|
when this method is used multiple times.
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of Role and created flag
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(role_name, str):
|
if not isinstance(role_name, str):
|
||||||
raise TypeError('role_name must be of type string')
|
raise TypeError('role_name must be of type string')
|
||||||
|
|
||||||
created = False
|
created = False
|
||||||
if guild_roles is None:
|
if guild_roles is None:
|
||||||
guild_roles = RolesSet(self.guild_roles(guild_id))
|
guild_roles = DiscordRoles(self.guild_roles(guild_id))
|
||||||
role = guild_roles.role_by_name(role_name)
|
role = guild_roles.role_by_name(role_name)
|
||||||
if not role:
|
if not role:
|
||||||
if not DISCORD_DISABLE_ROLE_CREATION:
|
if not DISCORD_DISABLE_ROLE_CREATION:
|
||||||
@@ -391,24 +345,9 @@ class DiscordClient:
|
|||||||
created = True
|
created = True
|
||||||
else:
|
else:
|
||||||
role = None
|
role = None
|
||||||
|
|
||||||
return role, created
|
return role, created
|
||||||
|
|
||||||
def match_or_create_roles_from_names_2(
|
|
||||||
self, guild_id: int, role_names: Iterable[str]
|
|
||||||
) -> RolesSet:
|
|
||||||
"""Fetch or create Discord role matching the given name.
|
|
||||||
|
|
||||||
Wrapper for ``match_or_create_role_from_name()``
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Roles as RolesSet object.
|
|
||||||
"""
|
|
||||||
return RolesSet.create_from_matched_roles(
|
|
||||||
self.match_or_create_roles_from_names(
|
|
||||||
guild_id=guild_id, role_names=role_names
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# guild members
|
# guild members
|
||||||
|
|
||||||
def add_guild_member(
|
def add_guild_member(
|
||||||
@@ -418,13 +357,13 @@ class DiscordClient:
|
|||||||
access_token: str,
|
access_token: str,
|
||||||
role_ids: list = None,
|
role_ids: list = None,
|
||||||
nick: str = None
|
nick: str = None
|
||||||
) -> Optional[bool]:
|
) -> bool:
|
||||||
"""Adds a user to the guild.
|
"""Adds a user to the guilds.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- True when a new user was added
|
- True when a new user was added
|
||||||
- None if the user already existed
|
- None if the user already existed
|
||||||
- False when something went wrong or raises exception
|
- False when something went wrong or raises exception
|
||||||
"""
|
"""
|
||||||
route = f"guilds/{guild_id}/members/{user_id}"
|
route = f"guilds/{guild_id}/members/{user_id}"
|
||||||
data = {
|
data = {
|
||||||
@@ -432,49 +371,42 @@ class DiscordClient:
|
|||||||
}
|
}
|
||||||
if role_ids:
|
if role_ids:
|
||||||
data['roles'] = self._sanitize_role_ids(role_ids)
|
data['roles'] = self._sanitize_role_ids(role_ids)
|
||||||
|
|
||||||
if nick:
|
if nick:
|
||||||
data['nick'] = GuildMember.sanitize_nick(nick)
|
data['nick'] = str(nick)[:self._NICK_MAX_CHARS]
|
||||||
|
|
||||||
r = self._api_request(method='put', route=route, data=data)
|
r = self._api_request(method='put', route=route, data=data)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
if r.status_code == 201:
|
if r.status_code == 201:
|
||||||
return True
|
return True
|
||||||
elif r.status_code == 204:
|
elif r.status_code == 204:
|
||||||
return None
|
return None
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def guild_member(self, guild_id: int, user_id: int) -> Optional[GuildMember]:
|
def guild_member(self, guild_id: int, user_id: int) -> dict:
|
||||||
"""Fetch info for a guild member.
|
"""returns the user info for a guild member
|
||||||
|
|
||||||
Args:
|
or None if the user is not a member of the guild
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
user_id: Discord ID of the user
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
guild member or ``None`` if the user is not a member of the guild
|
|
||||||
"""
|
"""
|
||||||
route = f'guilds/{guild_id}/members/{user_id}'
|
route = f'guilds/{guild_id}/members/{user_id}'
|
||||||
r = self._api_request(method='get', route=route, raise_for_status=False)
|
r = self._api_request(method='get', route=route, raise_for_status=False)
|
||||||
if self._is_member_unknown_error(r):
|
if self._is_member_unknown_error(r):
|
||||||
logger.warning("Discord user ID %s could not be found on server.", user_id)
|
logger.warning("Discord user ID %s could not be found on server.", user_id)
|
||||||
return None
|
return None
|
||||||
r.raise_for_status()
|
else:
|
||||||
return GuildMember.from_dict(r.json())
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
def modify_guild_member(
|
def modify_guild_member(
|
||||||
self, guild_id: int, user_id: int, role_ids: List[int] = None, nick: str = None
|
self, guild_id: int, user_id: int, role_ids: list = None, nick: str = None
|
||||||
) -> Optional[bool]:
|
) -> bool:
|
||||||
"""Set properties of a guild member.
|
"""Modify attributes of a guild member.
|
||||||
|
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
user_id: Discord ID of the user
|
|
||||||
roles_id: New list of role IDs (if provided)
|
|
||||||
nick: New nickname (if provided)
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
- True when successful
|
- True when successful
|
||||||
- None if user is not a member of this guild
|
- None if user is not a member of this guild
|
||||||
- False otherwise
|
- False otherwise
|
||||||
"""
|
"""
|
||||||
if not role_ids and not nick:
|
if not role_ids and not nick:
|
||||||
raise ValueError('Must specify role_ids or nick')
|
raise ValueError('Must specify role_ids or nick')
|
||||||
@@ -487,7 +419,7 @@ class DiscordClient:
|
|||||||
data['roles'] = self._sanitize_role_ids(role_ids)
|
data['roles'] = self._sanitize_role_ids(role_ids)
|
||||||
|
|
||||||
if nick:
|
if nick:
|
||||||
data['nick'] = GuildMember.sanitize_nick(nick)
|
data['nick'] = self._sanitize_nick(nick)
|
||||||
|
|
||||||
route = f"guilds/{guild_id}/members/{user_id}"
|
route = f"guilds/{guild_id}/members/{user_id}"
|
||||||
r = self._api_request(
|
r = self._api_request(
|
||||||
@@ -496,22 +428,21 @@ class DiscordClient:
|
|||||||
if self._is_member_unknown_error(r):
|
if self._is_member_unknown_error(r):
|
||||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||||
return None
|
return None
|
||||||
r.raise_for_status()
|
else:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
if r.status_code == 204:
|
if r.status_code == 204:
|
||||||
return True
|
return True
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def remove_guild_member(self, guild_id: int, user_id: int) -> Optional[bool]:
|
def remove_guild_member(self, guild_id: int, user_id: int) -> bool:
|
||||||
"""Remove a member from a guild.
|
"""Remove a member from a guild
|
||||||
|
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
user_id: Discord ID of the user
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- True when successful
|
- True when successful
|
||||||
- None if member does not exist
|
- None if member does not exist
|
||||||
- False otherwise
|
- False otherwise
|
||||||
"""
|
"""
|
||||||
route = f"guilds/{guild_id}/members/{user_id}"
|
route = f"guilds/{guild_id}/members/{user_id}"
|
||||||
r = self._api_request(
|
r = self._api_request(
|
||||||
@@ -520,16 +451,19 @@ class DiscordClient:
|
|||||||
if self._is_member_unknown_error(r):
|
if self._is_member_unknown_error(r):
|
||||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||||
return None
|
return None
|
||||||
r.raise_for_status()
|
else:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
if r.status_code == 204:
|
if r.status_code == 204:
|
||||||
return True
|
return True
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
# Guild member roles
|
# Guild member roles
|
||||||
|
|
||||||
def add_guild_member_role(
|
def add_guild_member_role(
|
||||||
self, guild_id: int, user_id: int, role_id: int
|
self, guild_id: int, user_id: int, role_id: int
|
||||||
) -> Optional[bool]:
|
) -> bool:
|
||||||
"""Adds a role to a guild member
|
"""Adds a role to a guild member
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -542,69 +476,43 @@ class DiscordClient:
|
|||||||
if self._is_member_unknown_error(r):
|
if self._is_member_unknown_error(r):
|
||||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||||
return None
|
return None
|
||||||
r.raise_for_status()
|
else:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
if r.status_code == 204:
|
if r.status_code == 204:
|
||||||
return True
|
return True
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def remove_guild_member_role(
|
def remove_guild_member_role(
|
||||||
self, guild_id: int, user_id: int, role_id: int
|
self, guild_id: int, user_id: int, role_id: int
|
||||||
) -> Optional[bool]:
|
) -> bool:
|
||||||
"""Remove a role to a guild member
|
"""Removes a role to a guild member
|
||||||
|
|
||||||
Args:
|
|
||||||
guild_id: Discord ID of the guild
|
|
||||||
user_id: Discord ID of the user
|
|
||||||
role_id: Discord ID of role to be removed
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- True when successful
|
- True when successful
|
||||||
- None if member does not exist
|
- None if member does not exist
|
||||||
- False otherwise
|
- False otherwise
|
||||||
"""
|
"""
|
||||||
route = f"guilds/{guild_id}/members/{user_id}/roles/{role_id}"
|
route = f"guilds/{guild_id}/members/{user_id}/roles/{role_id}"
|
||||||
r = self._api_request(method='delete', route=route, raise_for_status=False)
|
r = self._api_request(method='delete', route=route, raise_for_status=False)
|
||||||
if self._is_member_unknown_error(r):
|
if self._is_member_unknown_error(r):
|
||||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||||
return None
|
return None
|
||||||
r.raise_for_status()
|
else:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
if r.status_code == 204:
|
if r.status_code == 204:
|
||||||
return True
|
return True
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
def guild_member_roles(self, guild_id: int, user_id: int) -> Optional[RolesSet]:
|
|
||||||
"""Fetch the current guild roles of a guild member.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- guild_id: Discord guild ID
|
|
||||||
- user_id: Discord user ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- Member roles
|
|
||||||
- None if user is not a member of the guild
|
|
||||||
"""
|
|
||||||
member_info = self.guild_member(guild_id=guild_id, user_id=user_id)
|
|
||||||
if member_info is None:
|
|
||||||
return None # User is no longer a member
|
|
||||||
guild_roles = RolesSet(self.guild_roles(guild_id=guild_id))
|
|
||||||
logger.debug('Current guild roles: %s', guild_roles.ids())
|
|
||||||
if not guild_roles.has_roles(member_info.roles):
|
|
||||||
guild_roles = RolesSet(
|
|
||||||
self.guild_roles(guild_id=guild_id, use_cache=False)
|
|
||||||
)
|
|
||||||
if not guild_roles.has_roles(member_info.roles):
|
|
||||||
role_ids = set(member_info.roles).difference(guild_roles.ids())
|
|
||||||
raise RuntimeError(
|
|
||||||
f'Discord user {user_id} has unknown roles: {role_ids}'
|
|
||||||
)
|
|
||||||
return guild_roles.subset(member_info.roles)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _is_member_unknown_error(cls, r: requests.Response) -> bool:
|
def _is_member_unknown_error(cls, r: requests.Response) -> bool:
|
||||||
try:
|
try:
|
||||||
result = (
|
result = (
|
||||||
r.status_code == HTTPStatus.NOT_FOUND
|
r.status_code == cls._HTTP_STATUS_CODE_NOT_FOUND
|
||||||
and r.json()['code'] == DiscordApiStatusCode.UNKNOWN_MEMBER
|
and r.json()['code'] == cls._DISCORD_STATUS_CODE_UNKNOWN_MEMBER
|
||||||
)
|
)
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
result = False
|
result = False
|
||||||
@@ -621,19 +529,7 @@ class DiscordClient:
|
|||||||
authorization: str = None,
|
authorization: str = None,
|
||||||
raise_for_status: bool = True
|
raise_for_status: bool = True
|
||||||
) -> requests.Response:
|
) -> requests.Response:
|
||||||
"""Core method for performing all API calls.
|
"""Core method for performing all API calls"""
|
||||||
|
|
||||||
Args:
|
|
||||||
method: HTTP method of the request, e.g. "get"
|
|
||||||
route: Route in the Discord API, e.g. "users/@me"
|
|
||||||
data: Data to be send with the request
|
|
||||||
authorization: The authorization string to be used.
|
|
||||||
Will use the default bot token if not set.
|
|
||||||
raise_for_status: Whether a requests exception is to be raised when not ok
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The raw response from the API
|
|
||||||
"""
|
|
||||||
uid = uuid1().hex
|
uid = uuid1().hex
|
||||||
|
|
||||||
if not hasattr(requests, method):
|
if not hasattr(requests, method):
|
||||||
@@ -681,7 +577,7 @@ class DiscordClient:
|
|||||||
r.text
|
r.text
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
if r.status_code == self._HTTP_STATUS_CODE_RATE_LIMITED:
|
||||||
self._handle_new_api_backoff(r, uid)
|
self._handle_new_api_backoff(r, uid)
|
||||||
|
|
||||||
self._report_rate_limit_from_api(r, uid)
|
self._report_rate_limit_from_api(r, uid)
|
||||||
@@ -692,10 +588,9 @@ class DiscordClient:
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
def _handle_ongoing_api_backoff(self, uid: str) -> None:
|
def _handle_ongoing_api_backoff(self, uid: str) -> None:
|
||||||
"""Check if api is currently on backoff.
|
"""checks if api is currently on backoff
|
||||||
|
if on backoff: will do a blocking wait if it expires soon,
|
||||||
If on backoff: will do a blocking wait if it expires soon,
|
else raises exception
|
||||||
else raises exception.
|
|
||||||
"""
|
"""
|
||||||
global_backoff_duration = self._redis.pttl(self._KEY_GLOBAL_BACKOFF_UNTIL)
|
global_backoff_duration = self._redis.pttl(self._KEY_GLOBAL_BACKOFF_UNTIL)
|
||||||
if global_backoff_duration > 0:
|
if global_backoff_duration > 0:
|
||||||
@@ -715,9 +610,8 @@ class DiscordClient:
|
|||||||
raise DiscordTooManyRequestsError(retry_after=global_backoff_duration)
|
raise DiscordTooManyRequestsError(retry_after=global_backoff_duration)
|
||||||
|
|
||||||
def _ensure_rate_limed_not_exhausted(self, uid: str) -> int:
|
def _ensure_rate_limed_not_exhausted(self, uid: str) -> int:
|
||||||
"""Ensures that the rate limit is not exhausted.
|
"""ensures that the rate limit is not exhausted
|
||||||
|
if exhausted: will do a blocking wait if rate limit resets soon,
|
||||||
If exhausted: will do a blocking wait if rate limit resets soon,
|
|
||||||
else raises exception
|
else raises exception
|
||||||
|
|
||||||
returns requests remaining on success
|
returns requests remaining on success
|
||||||
@@ -760,10 +654,10 @@ class DiscordClient:
|
|||||||
)
|
)
|
||||||
raise DiscordRateLimitExhausted(resets_in)
|
raise DiscordRateLimitExhausted(resets_in)
|
||||||
|
|
||||||
raise RuntimeError('Failed to handle rate limit after after too many tries.')
|
raise RuntimeError('Failed to handle rate limit after after too tries.')
|
||||||
|
|
||||||
def _handle_new_api_backoff(self, r: requests.Response, uid: str) -> None:
|
def _handle_new_api_backoff(self, r: requests.Response, uid: str) -> None:
|
||||||
"""Raise exception for new API backoff error."""
|
"""raises exception for new API backoff error"""
|
||||||
response = r.json()
|
response = r.json()
|
||||||
if 'retry_after' in response:
|
if 'retry_after' in response:
|
||||||
try:
|
try:
|
||||||
@@ -785,8 +679,8 @@ class DiscordClient:
|
|||||||
)
|
)
|
||||||
raise DiscordTooManyRequestsError(retry_after=retry_after)
|
raise DiscordTooManyRequestsError(retry_after=retry_after)
|
||||||
|
|
||||||
def _report_rate_limit_from_api(self, r, uid) -> None:
|
def _report_rate_limit_from_api(self, r, uid):
|
||||||
"""Try to log the current rate limit reported from API."""
|
"""Tries to log the current rate limit reported from API"""
|
||||||
if (
|
if (
|
||||||
logger.getEffectiveLevel() <= logging.DEBUG
|
logger.getEffectiveLevel() <= logging.DEBUG
|
||||||
and 'x-ratelimit-limit' in r.headers
|
and 'x-ratelimit-limit' in r.headers
|
||||||
@@ -809,17 +703,22 @@ class DiscordClient:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _redis_decode(value: str) -> str:
|
def _redis_decode(value: str) -> str:
|
||||||
"""Decode a string from Redis and passes through None and Booleans."""
|
"""Decodes a string from Redis and passes through None and Booleans"""
|
||||||
if value is not None and not isinstance(value, bool):
|
if value is not None and not isinstance(value, bool):
|
||||||
return value.decode('utf-8')
|
return value.decode('utf-8')
|
||||||
return value
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_hash(key: str) -> str:
|
def _generate_hash(key: str) -> str:
|
||||||
"""Generate hash key for given string."""
|
|
||||||
return md5(key.encode('utf-8')).hexdigest()
|
return md5(key.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_role_ids(role_ids: Iterable[int]) -> List[int]:
|
def _sanitize_role_ids(role_ids: list) -> list:
|
||||||
"""Sanitize a list of role IDs, i.e. make sure its a list of unique integers."""
|
"""make sure its a list of integers"""
|
||||||
return [int(role_id) for role_id in set(role_ids)]
|
return [int(role_id) for role_id in list(role_ids)]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _sanitize_nick(cls, nick: str) -> str:
|
||||||
|
"""shortens too long strings if necessary"""
|
||||||
|
return str(nick)[:cls._NICK_MAX_CHARS]
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
"""Custom exceptions for the Discord Client package."""
|
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
class DiscordClientException(Exception):
|
class DiscordClientException(Exception):
|
||||||
"""Base Exception for the Discord client."""
|
"""Base Exception for the Discord client"""
|
||||||
|
|
||||||
|
|
||||||
class DiscordApiBackoff(DiscordClientException):
|
class DiscordApiBackoff(DiscordClientException):
|
||||||
"""Exception signaling we need to backoff from sending requests to the API for now.
|
"""Exception signaling we need to backoff from sending requests to the API for now
|
||||||
|
|
||||||
Args:
|
|
||||||
retry_after: time to retry after in milliseconds
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, retry_after: int):
|
def __init__(self, retry_after: int):
|
||||||
|
"""
|
||||||
|
:param retry_after: int time to retry after in milliseconds
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.retry_after = int(retry_after)
|
self.retry_after = int(retry_after)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def retry_after_seconds(self):
|
def retry_after_seconds(self):
|
||||||
"""Time to retry after in seconds."""
|
|
||||||
return math.ceil(self.retry_after / 1000)
|
return math.ceil(self.retry_after / 1000)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,27 @@
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from typing import Iterable, List, Optional, Set, Tuple
|
from typing import Set, Iterable
|
||||||
|
|
||||||
from .models import Role
|
|
||||||
|
|
||||||
|
|
||||||
class RolesSet:
|
class DiscordRoles:
|
||||||
"""Container of Discord roles with added functionality.
|
"""Container class that helps dealing with Discord roles.
|
||||||
|
|
||||||
Objects of this class are immutable and work in many ways like sets.
|
Objects of this class are immutable and work in many ways like sets.
|
||||||
|
|
||||||
Ideally objects are initialized from raw API responses,
|
Ideally objects are initialized from raw API responses,
|
||||||
e.g. from DiscordClient.guild.roles().
|
e.g. from DiscordClient.guild.roles()
|
||||||
|
|
||||||
Args:
|
|
||||||
roles_lst: List of dicts, each defining a role
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, roles_lst: Iterable[Role]) -> None:
|
_ROLE_NAME_MAX_CHARS = 100
|
||||||
|
|
||||||
|
def __init__(self, roles_lst: list) -> None:
|
||||||
|
"""roles_lst must be a list of dict, each defining a role"""
|
||||||
if not isinstance(roles_lst, (list, set, tuple)):
|
if not isinstance(roles_lst, (list, set, tuple)):
|
||||||
raise TypeError('roles_lst must be of type list, set or tuple')
|
raise TypeError('roles_lst must be of type list, set or tuple')
|
||||||
self._roles = dict()
|
self._roles = dict()
|
||||||
self._roles_by_name = dict()
|
self._roles_by_name = dict()
|
||||||
for role in list(roles_lst):
|
for role in list(roles_lst):
|
||||||
if not isinstance(role, Role):
|
self._assert_valid_role(role)
|
||||||
raise TypeError('Roles must be of type Role: %s' % role)
|
self._roles[int(role['id'])] = role
|
||||||
self._roles[role.id] = role
|
self._roles_by_name[self.sanitize_role_name(role['name'])] = role
|
||||||
self._roles_by_name[role.name] = role
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
if self._roles_by_name:
|
|
||||||
roles = '"' + '", "'.join(sorted(list(self._roles_by_name.keys()))) + '"'
|
|
||||||
else:
|
|
||||||
roles = ""
|
|
||||||
return f'{self.__class__.__name__}([{roles}])'
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, type(self)):
|
if isinstance(other, type(self)):
|
||||||
@@ -51,15 +41,15 @@ class RolesSet:
|
|||||||
return len(self._roles.keys())
|
return len(self._roles.keys())
|
||||||
|
|
||||||
def has_roles(self, role_ids: Set[int]) -> bool:
|
def has_roles(self, role_ids: Set[int]) -> bool:
|
||||||
"""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
|
||||||
"""
|
"""
|
||||||
role_ids = {int(id) for id in role_ids}
|
role_ids = {int(id) for id in role_ids}
|
||||||
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[int]:
|
def ids(self) -> Set[int]:
|
||||||
"""Set of all role IDs."""
|
"""return a set of all role IDs"""
|
||||||
return set(self._roles.keys())
|
return set(self._roles.keys())
|
||||||
|
|
||||||
def subset(
|
def subset(
|
||||||
@@ -67,13 +57,13 @@ class RolesSet:
|
|||||||
role_ids: Iterable[int] = None,
|
role_ids: Iterable[int] = None,
|
||||||
managed_only: bool = False,
|
managed_only: bool = False,
|
||||||
role_names: Iterable[str] = None
|
role_names: Iterable[str] = None
|
||||||
) -> "RolesSet":
|
) -> "DiscordRoles":
|
||||||
"""Create instance containing the subset of roles
|
"""returns a new object containing the subset of roles
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
role_ids: role ids must be in the provided list
|
- role_ids: role ids must be in the provided list
|
||||||
managed_only: roles must be managed
|
- managed_only: roles must be managed
|
||||||
role_names: role names must match provided list (not case sensitive)
|
- 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}
|
||||||
@@ -85,50 +75,72 @@ class RolesSet:
|
|||||||
|
|
||||||
elif role_ids is None and managed_only:
|
elif role_ids is None and managed_only:
|
||||||
return type(self)([
|
return type(self)([
|
||||||
role for _, role in self._roles.items() if role.managed
|
role for _, role in self._roles.items() if role['managed']
|
||||||
])
|
])
|
||||||
|
|
||||||
elif role_ids is not None and managed_only:
|
elif role_ids is not None and managed_only:
|
||||||
return type(self)([
|
return type(self)([
|
||||||
role for role_id, role in self._roles.items()
|
role for role_id, role in self._roles.items()
|
||||||
if role_id in role_ids and role.managed
|
if role_id in role_ids and role['managed']
|
||||||
])
|
])
|
||||||
|
|
||||||
elif role_ids is None and managed_only is False and role_names is not None:
|
elif role_ids is None and managed_only is False and role_names is not None:
|
||||||
role_names = {Role.sanitize_name(name).lower() for name in role_names}
|
role_names = {self.sanitize_role_name(name).lower() for name in role_names}
|
||||||
return type(self)([
|
return type(self)([
|
||||||
role for role in self._roles.values()
|
role for role in self._roles.values()
|
||||||
if role.name.lower() in role_names
|
if role["name"].lower() in role_names
|
||||||
])
|
])
|
||||||
|
|
||||||
return copy(self)
|
return copy(self)
|
||||||
|
|
||||||
def union(self, other: object) -> "RolesSet":
|
def union(self, other: object) -> "DiscordRoles":
|
||||||
"""Create instance that is the union of this roles object with other."""
|
"""returns a new roles object that is the union of this roles object
|
||||||
|
with other"""
|
||||||
return type(self)(list(self) + list(other))
|
return type(self)(list(self) + list(other))
|
||||||
|
|
||||||
def difference(self, other: object) -> "RolesSet":
|
def difference(self, other: object) -> "DiscordRoles":
|
||||||
"""Create instance 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
|
||||||
"""
|
"""
|
||||||
new_ids = self.ids().difference(other.ids())
|
new_ids = self.ids().difference(other.ids())
|
||||||
return self.subset(role_ids=new_ids)
|
return self.subset(role_ids=new_ids)
|
||||||
|
|
||||||
def role_by_name(self, role_name: str) -> Optional[Role]:
|
def role_by_name(self, role_name: str) -> dict:
|
||||||
"""Role if one with matching name is found else None."""
|
"""returns role if one with matching name is found else an empty dict"""
|
||||||
role_name = Role.sanitize_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]
|
||||||
return None
|
return dict()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_matched_roles(
|
def create_from_matched_roles(cls, matched_roles: list) -> "DiscordRoles":
|
||||||
cls, matched_roles: List[Tuple[Role, bool]]
|
"""returns a new object created from the given list of matches roles
|
||||||
) -> "RolesSet":
|
|
||||||
"""Create new instance from the given list of matches roles.
|
|
||||||
|
|
||||||
Args:
|
matches_roles must be a list of tuples in the form: (role, created)
|
||||||
matches_roles: list of matches roles
|
|
||||||
"""
|
"""
|
||||||
raw_roles = [x[0] for x in matched_roles]
|
raw_roles = [x[0] for x in matched_roles]
|
||||||
return cls(raw_roles)
|
return cls(raw_roles)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _assert_valid_role(role: dict) -> None:
|
||||||
|
if not isinstance(role, dict):
|
||||||
|
raise TypeError('Roles must be of type dict: %s' % role)
|
||||||
|
|
||||||
|
if 'id' not in role or 'name' not in role or 'managed' not in role:
|
||||||
|
raise ValueError('This role is not valid: %s' % role)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sanitize_role_name(cls, role_name: str) -> str:
|
||||||
|
"""shortens too long strings if necessary"""
|
||||||
|
return str(role_name)[:cls._ROLE_NAME_MAX_CHARS]
|
||||||
|
|
||||||
|
|
||||||
|
def match_or_create_roles_from_names(
|
||||||
|
client: object, guild_id: int, role_names: list
|
||||||
|
) -> DiscordRoles:
|
||||||
|
"""Shortcut for getting the result of matching role names as DiscordRoles object"""
|
||||||
|
return DiscordRoles.create_from_matched_roles(
|
||||||
|
client.match_or_create_roles_from_names(
|
||||||
|
guild_id=guild_id, role_names=role_names
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
"""Implementation of Discord objects used by this client.
|
|
||||||
|
|
||||||
Note that only those objects and properties are implemented, which are needed by AA.
|
|
||||||
|
|
||||||
Names and types are mirrored from the API whenever possible.
|
|
||||||
Discord's snowflake type (used by Discord IDs) is implemented as int.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass
|
|
||||||
from typing import FrozenSet
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class User:
|
|
||||||
"""A user on Discord."""
|
|
||||||
|
|
||||||
id: int
|
|
||||||
username: str
|
|
||||||
discriminator: str
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
object.__setattr__(self, "id", int(self.id))
|
|
||||||
object.__setattr__(self, "username", str(self.username))
|
|
||||||
object.__setattr__(self, "discriminator", str(self.discriminator))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> "User":
|
|
||||||
"""Create object from dictionary as received from the API."""
|
|
||||||
return cls(
|
|
||||||
id=int(data["id"]),
|
|
||||||
username=data["username"],
|
|
||||||
discriminator=data["discriminator"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Role:
|
|
||||||
"""A role on Discord."""
|
|
||||||
|
|
||||||
_ROLE_NAME_MAX_CHARS = 100
|
|
||||||
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
managed: bool = False
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
object.__setattr__(self, "id", int(self.id))
|
|
||||||
object.__setattr__(self, "name", self.sanitize_name(self.name))
|
|
||||||
object.__setattr__(self, "managed", bool(self.managed))
|
|
||||||
|
|
||||||
def asdict(self) -> dict:
|
|
||||||
"""Convert object into a dictionary representation."""
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> "Role":
|
|
||||||
"""Create object from dictionary as received from the API."""
|
|
||||||
return cls(id=int(data["id"]), name=data["name"], managed=data["managed"])
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sanitize_name(cls, role_name: str) -> str:
|
|
||||||
"""Shorten too long names if necessary."""
|
|
||||||
return str(role_name)[: cls._ROLE_NAME_MAX_CHARS]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Guild:
|
|
||||||
"""A guild on Discord."""
|
|
||||||
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
roles: FrozenSet[Role]
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
object.__setattr__(self, "id", int(self.id))
|
|
||||||
object.__setattr__(self, "name", str(self.name))
|
|
||||||
for role in self.roles:
|
|
||||||
if not isinstance(role, Role):
|
|
||||||
raise TypeError("roles can only contain Role objects.")
|
|
||||||
object.__setattr__(self, "roles", frozenset(self.roles))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> "Guild":
|
|
||||||
"""Create object from dictionary as received from the API."""
|
|
||||||
return cls(
|
|
||||||
id=int(data["id"]),
|
|
||||||
name=data["name"],
|
|
||||||
roles=frozenset(Role.from_dict(obj) for obj in data["roles"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class GuildMember:
|
|
||||||
"""A member of a guild on Discord."""
|
|
||||||
|
|
||||||
_NICK_MAX_CHARS = 32
|
|
||||||
|
|
||||||
roles: FrozenSet[int]
|
|
||||||
nick: str = None
|
|
||||||
user: User = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.nick:
|
|
||||||
object.__setattr__(self, "nick", self.sanitize_nick(self.nick))
|
|
||||||
if self.user and not isinstance(self.user, User):
|
|
||||||
raise TypeError("user must be of type User")
|
|
||||||
for role in self.roles:
|
|
||||||
if not isinstance(role, int):
|
|
||||||
raise TypeError("roles can only contain ints")
|
|
||||||
object.__setattr__(self, "roles", frozenset(self.roles))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> "GuildMember":
|
|
||||||
"""Create object from dictionary as received from the API."""
|
|
||||||
params = {"roles": {int(obj) for obj in data["roles"]}}
|
|
||||||
if data.get("user"):
|
|
||||||
params["user"] = User.from_dict(data["user"])
|
|
||||||
if data.get("nick"):
|
|
||||||
params["nick"] = data["nick"]
|
|
||||||
return cls(**params)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sanitize_nick(cls, nick: str) -> str:
|
|
||||||
"""Sanitize a nick, i.e. shorten too long strings if necessary."""
|
|
||||||
return str(nick)[: cls._NICK_MAX_CHARS]
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
TEST_GUILD_ID = 123456789012345678
|
||||||
|
TEST_USER_ID = 198765432012345678
|
||||||
|
TEST_USER_NAME = 'Peter Parker'
|
||||||
|
TEST_USER_DISCRIMINATOR = '1234'
|
||||||
|
TEST_BOT_TOKEN = 'abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY'
|
||||||
|
TEST_ROLE_ID = 654321012345678912
|
||||||
|
|
||||||
|
|
||||||
|
def create_role(id: int, name: str, managed=False) -> dict:
|
||||||
|
return {
|
||||||
|
'id': int(id),
|
||||||
|
'name': str(name),
|
||||||
|
'managed': bool(managed)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_matched_role(role, created=False) -> tuple:
|
||||||
|
return role, created
|
||||||
|
|
||||||
|
|
||||||
|
ROLE_ALPHA = create_role(1, 'alpha')
|
||||||
|
ROLE_BRAVO = create_role(2, 'bravo')
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
ALL_ROLES = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_info(
|
||||||
|
id: int = TEST_USER_ID,
|
||||||
|
username: str = TEST_USER_NAME,
|
||||||
|
discriminator: str = TEST_USER_DISCRIMINATOR
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
'id': str(id),
|
||||||
|
'username': str(username[:32]),
|
||||||
|
'discriminator': str(discriminator[:4])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
{
|
|
||||||
"guilds": {
|
|
||||||
"2909267986263572999": {
|
|
||||||
"id": "2909267986263572999",
|
|
||||||
"name": "Mason's Test Server",
|
|
||||||
"icon": "389030ec9db118cb5b85a732333b7c98",
|
|
||||||
"description": null,
|
|
||||||
"splash": "75610b05a0dd09ec2c3c7df9f6975ea0",
|
|
||||||
"discovery_splash": null,
|
|
||||||
"approximate_member_count": 2,
|
|
||||||
"approximate_presence_count": 2,
|
|
||||||
"features": [
|
|
||||||
"INVITE_SPLASH",
|
|
||||||
"VANITY_URL",
|
|
||||||
"COMMERCE",
|
|
||||||
"BANNER",
|
|
||||||
"NEWS",
|
|
||||||
"VERIFIED",
|
|
||||||
"VIP_REGIONS"
|
|
||||||
],
|
|
||||||
"emojis": [
|
|
||||||
{
|
|
||||||
"name": "ultrafastparrot",
|
|
||||||
"roles": [],
|
|
||||||
"id": "393564762228785161",
|
|
||||||
"require_colons": true,
|
|
||||||
"managed": false,
|
|
||||||
"animated": true,
|
|
||||||
"available": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"banner": "5c3cb8d1bc159937fffe7e641ec96ca7",
|
|
||||||
"owner_id": "53908232506183680",
|
|
||||||
"application_id": null,
|
|
||||||
"region": null,
|
|
||||||
"afk_channel_id": null,
|
|
||||||
"afk_timeout": 300,
|
|
||||||
"system_channel_id": null,
|
|
||||||
"widget_enabled": true,
|
|
||||||
"widget_channel_id": "639513352485470208",
|
|
||||||
"verification_level": 0,
|
|
||||||
"roles": [
|
|
||||||
{
|
|
||||||
"id": "2909267986263572999",
|
|
||||||
"name": "@everyone",
|
|
||||||
"permissions": "49794752",
|
|
||||||
"position": 0,
|
|
||||||
"color": 0,
|
|
||||||
"hoist": false,
|
|
||||||
"managed": false,
|
|
||||||
"mentionable": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default_message_notifications": 1,
|
|
||||||
"mfa_level": 0,
|
|
||||||
"explicit_content_filter": 0,
|
|
||||||
"max_presences": null,
|
|
||||||
"max_members": 250000,
|
|
||||||
"max_video_channel_users": 25,
|
|
||||||
"vanity_url_code": "no",
|
|
||||||
"premium_tier": 0,
|
|
||||||
"premium_subscription_count": 0,
|
|
||||||
"system_channel_flags": 0,
|
|
||||||
"preferred_locale": "en-US",
|
|
||||||
"rules_channel_id": null,
|
|
||||||
"public_updates_channel_id": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"guildMembers": {
|
|
||||||
"1": {
|
|
||||||
"user": {},
|
|
||||||
"nick": null,
|
|
||||||
"avatar": null,
|
|
||||||
"roles": [],
|
|
||||||
"joined_at": "2015-04-26T06:26:56.936000+00:00",
|
|
||||||
"deaf": false,
|
|
||||||
"mute": false
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"user": {
|
|
||||||
"id": "80351110224678912",
|
|
||||||
"username": "Nelly",
|
|
||||||
"discriminator": "1337",
|
|
||||||
"avatar": "8342729096ea3675442027381ff50dfe",
|
|
||||||
"verified": true,
|
|
||||||
"email": "nelly@discord.com",
|
|
||||||
"flags": 64,
|
|
||||||
"banner": "06c16474723fe537c283b8efa61a30c8",
|
|
||||||
"accent_color": 16711680,
|
|
||||||
"premium_type": 1,
|
|
||||||
"public_flags": 64
|
|
||||||
},
|
|
||||||
"nick": "Nelly the great",
|
|
||||||
"avatar": null,
|
|
||||||
"roles": [
|
|
||||||
"197150972374548480",
|
|
||||||
"41771983423143936"
|
|
||||||
],
|
|
||||||
"joined_at": "2015-04-26T06:26:56.936000+00:00",
|
|
||||||
"deaf": false,
|
|
||||||
"mute": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"roles": {
|
|
||||||
"197150972374548480": {
|
|
||||||
"id": "197150972374548480",
|
|
||||||
"name": "My Managed Role",
|
|
||||||
"color": 3447003,
|
|
||||||
"hoist": false,
|
|
||||||
"icon": "cf3ced8600b777c9486c6d8d84fb4327",
|
|
||||||
"unicode_emoji": null,
|
|
||||||
"position": 2,
|
|
||||||
"permissions": "66321471",
|
|
||||||
"managed": true,
|
|
||||||
"mentionable": false
|
|
||||||
},
|
|
||||||
"2909267986263572999": {
|
|
||||||
"id": "2909267986263572999",
|
|
||||||
"name": "@everyone",
|
|
||||||
"permissions": "49794752",
|
|
||||||
"position": 0,
|
|
||||||
"color": 0,
|
|
||||||
"hoist": false,
|
|
||||||
"managed": false,
|
|
||||||
"mentionable": false
|
|
||||||
},
|
|
||||||
"41771983423143936": {
|
|
||||||
"id": "41771983423143936",
|
|
||||||
"name": "WE DEM BOYZZ!!!!!!",
|
|
||||||
"color": 3447003,
|
|
||||||
"hoist": true,
|
|
||||||
"icon": "cf3ced8600b777c9486c6d8d84fb4327",
|
|
||||||
"unicode_emoji": null,
|
|
||||||
"position": 1,
|
|
||||||
"permissions": "66321471",
|
|
||||||
"managed": false,
|
|
||||||
"mentionable": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"80351110224678912": {
|
|
||||||
"id": "80351110224678912",
|
|
||||||
"username": "Nelly",
|
|
||||||
"discriminator": "1337",
|
|
||||||
"avatar": "8342729096ea3675442027381ff50dfe",
|
|
||||||
"verified": true,
|
|
||||||
"email": "nelly@discord.com",
|
|
||||||
"flags": 64,
|
|
||||||
"banner": "06c16474723fe537c283b8efa61a30c8",
|
|
||||||
"accent_color": 16711680,
|
|
||||||
"premium_type": 1,
|
|
||||||
"public_flags": 64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
from itertools import count
|
|
||||||
|
|
||||||
from django.utils.timezone import now
|
|
||||||
|
|
||||||
from ..client import DiscordApiStatusCode
|
|
||||||
from ..models import Guild, GuildMember, Role, User
|
|
||||||
|
|
||||||
TEST_GUILD_ID = 123456789012345678
|
|
||||||
TEST_GUILD_NAME = "Test Guild"
|
|
||||||
TEST_USER_ID = 198765432012345678
|
|
||||||
TEST_USER_NAME = "Peter Parker"
|
|
||||||
TEST_USER_DISCRIMINATOR = "1234"
|
|
||||||
TEST_BOT_TOKEN = "abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY"
|
|
||||||
TEST_ROLE_ID = 654321012345678912
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_role_object(id: int, name: str, managed: bool = False) -> dict:
|
|
||||||
return {"id": str(int(id)), "name": str(name), "managed": bool(managed)}
|
|
||||||
|
|
||||||
|
|
||||||
def create_matched_role(role, created=False) -> tuple:
|
|
||||||
return role, created
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_user_object(**kwargs):
|
|
||||||
params = {
|
|
||||||
"id": TEST_USER_ID,
|
|
||||||
"username": TEST_USER_NAME,
|
|
||||||
"discriminator": TEST_USER_DISCRIMINATOR,
|
|
||||||
}
|
|
||||||
params.update(kwargs)
|
|
||||||
params["id"] = str(int(params["id"]))
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_guild_member_object(user=None, **kwargs):
|
|
||||||
user_params = {}
|
|
||||||
if user:
|
|
||||||
user_params["user"] = user
|
|
||||||
params = {
|
|
||||||
"user": create_discord_user_object(**user_params),
|
|
||||||
"roles": [],
|
|
||||||
"joined_at": now().isoformat(),
|
|
||||||
"deaf": False,
|
|
||||||
"mute": False,
|
|
||||||
}
|
|
||||||
params.update(kwargs)
|
|
||||||
params["roles"] = [str(int(obj)) for obj in params["roles"]]
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_error_response(code: int) -> dict:
|
|
||||||
return {"code": int(code)}
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_error_response_unknown_member() -> dict:
|
|
||||||
return create_discord_error_response(DiscordApiStatusCode.UNKNOWN_MEMBER.value)
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_guild_object(**kwargs):
|
|
||||||
params = {"id": TEST_GUILD_ID, "name": TEST_GUILD_NAME, "roles": []}
|
|
||||||
params.update(kwargs)
|
|
||||||
params["id"] = str(int(params["id"]))
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def create_user(**kwargs):
|
|
||||||
params = {
|
|
||||||
"id": TEST_USER_ID,
|
|
||||||
"username": TEST_USER_NAME,
|
|
||||||
"discriminator": TEST_USER_DISCRIMINATOR,
|
|
||||||
}
|
|
||||||
params.update(kwargs)
|
|
||||||
return User(**params)
|
|
||||||
|
|
||||||
|
|
||||||
def create_guild(**kwargs):
|
|
||||||
params = {"id": TEST_GUILD_ID, "name": TEST_GUILD_NAME, "roles": []}
|
|
||||||
params.update(kwargs)
|
|
||||||
return Guild(**params)
|
|
||||||
|
|
||||||
|
|
||||||
def create_guild_member(**kwargs):
|
|
||||||
params = {"user": create_user(), "roles": []}
|
|
||||||
params.update(kwargs)
|
|
||||||
return GuildMember(**params)
|
|
||||||
|
|
||||||
|
|
||||||
def create_role(**kwargs) -> dict:
|
|
||||||
params = {"managed": False}
|
|
||||||
params.update(kwargs)
|
|
||||||
if "id" not in params:
|
|
||||||
params["id"] = next_number("role")
|
|
||||||
if "name" not in params:
|
|
||||||
params["name"] = f"Test Role #{params['id']}"
|
|
||||||
return Role(**params)
|
|
||||||
|
|
||||||
|
|
||||||
def next_number(key: str = None) -> int:
|
|
||||||
"""Calculate the next number in a persistent sequence."""
|
|
||||||
if key is None:
|
|
||||||
key = "_general"
|
|
||||||
try:
|
|
||||||
return next_number._counter[key].__next__()
|
|
||||||
except AttributeError:
|
|
||||||
next_number._counter = dict()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
next_number._counter[key] = count(start=1)
|
|
||||||
return next_number._counter[key].__next__()
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,201 +1,177 @@
|
|||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from ..helpers import RolesSet
|
from . import (
|
||||||
from .factories import create_matched_role, create_role
|
ROLE_ALPHA,
|
||||||
|
ROLE_BRAVO,
|
||||||
|
ROLE_CHARLIE,
|
||||||
|
ROLE_CHARLIE_2,
|
||||||
|
ROLE_MIKE,
|
||||||
|
ALL_ROLES,
|
||||||
|
create_role
|
||||||
|
)
|
||||||
|
from .. import DiscordRoles
|
||||||
|
|
||||||
|
|
||||||
class TestRolesSet(NoSocketsTestCase):
|
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiscordRoles(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||||
|
|
||||||
def test_can_create_simple(self):
|
def test_can_create_simple(self):
|
||||||
# given
|
roles_raw = [ROLE_ALPHA]
|
||||||
roles_raw = [create_role()]
|
roles = DiscordRoles(roles_raw)
|
||||||
# when
|
|
||||||
roles = RolesSet(roles_raw)
|
|
||||||
# then
|
|
||||||
self.assertListEqual(list(roles), roles_raw)
|
self.assertListEqual(list(roles), roles_raw)
|
||||||
|
|
||||||
def test_can_create_empty(self):
|
def test_can_create_empty(self):
|
||||||
# when
|
roles_raw = []
|
||||||
roles = RolesSet([])
|
roles = DiscordRoles(roles_raw)
|
||||||
# then
|
|
||||||
self.assertListEqual(list(roles), [])
|
self.assertListEqual(list(roles), [])
|
||||||
|
|
||||||
def test_raises_exception_if_roles_raw_of_wrong_type(self):
|
def test_raises_exception_if_roles_raw_of_wrong_type(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
RolesSet({"id": 1})
|
DiscordRoles({'id': 1})
|
||||||
|
|
||||||
def test_raises_exception_if_list_contains_non_dict(self):
|
def test_raises_exception_if_list_contains_non_dict(self):
|
||||||
# given
|
roles_raw = [ROLE_ALPHA, 'not_valid']
|
||||||
roles_raw = [create_role(), "not_valid"]
|
|
||||||
# when/then
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
RolesSet(roles_raw)
|
DiscordRoles(roles_raw)
|
||||||
|
|
||||||
|
def test_raises_exception_if_invalid_role_1(self):
|
||||||
|
roles_raw = [{'name': 'alpha', 'managed': False}]
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
DiscordRoles(roles_raw)
|
||||||
|
|
||||||
|
def test_raises_exception_if_invalid_role_2(self):
|
||||||
|
roles_raw = [{'id': 1, 'managed': False}]
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
DiscordRoles(roles_raw)
|
||||||
|
|
||||||
|
def test_raises_exception_if_invalid_role_3(self):
|
||||||
|
roles_raw = [{'id': 1, 'name': 'alpha'}]
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
DiscordRoles(roles_raw)
|
||||||
|
|
||||||
def test_roles_are_equal(self):
|
def test_roles_are_equal(self):
|
||||||
# given
|
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_b = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_b = create_role()
|
|
||||||
roles_a = RolesSet([role_a, role_b])
|
|
||||||
roles_b = RolesSet([role_a, role_b])
|
|
||||||
# when/then
|
|
||||||
self.assertEqual(roles_a, roles_b)
|
self.assertEqual(roles_a, roles_b)
|
||||||
|
|
||||||
def test_roles_are_not_equal(self):
|
def test_roles_are_not_equal(self):
|
||||||
# given
|
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_b = DiscordRoles([ROLE_ALPHA])
|
||||||
role_b = create_role()
|
|
||||||
roles_a = RolesSet([role_a, role_b])
|
|
||||||
roles_b = RolesSet([role_a])
|
|
||||||
# when/then
|
|
||||||
self.assertNotEqual(roles_a, roles_b)
|
self.assertNotEqual(roles_a, roles_b)
|
||||||
|
|
||||||
def test_different_objects_are_not_equal(self):
|
def test_different_objects_are_not_equal(self):
|
||||||
roles_a = RolesSet([])
|
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
self.assertFalse(roles_a == "invalid")
|
self.assertFalse(roles_a == "invalid")
|
||||||
|
|
||||||
def test_len(self):
|
def test_len(self):
|
||||||
# given
|
self.assertEqual(len(self.all_roles), 4)
|
||||||
role_a = create_role()
|
|
||||||
role_b = create_role()
|
|
||||||
roles = RolesSet([role_a, role_b])
|
|
||||||
# when/then
|
|
||||||
self.assertEqual(len(roles), 2)
|
|
||||||
|
|
||||||
def test_contains(self):
|
def test_contains(self):
|
||||||
# given
|
self.assertTrue(1 in self.all_roles)
|
||||||
role_a = create_role(id=1)
|
self.assertFalse(99 in self.all_roles)
|
||||||
roles = RolesSet([role_a])
|
|
||||||
# when/then
|
def test_sanitize_role_name(self):
|
||||||
self.assertTrue(1 in roles)
|
role_name_input = 'x' * 110
|
||||||
self.assertFalse(99 in roles)
|
role_name_expected = 'x' * 100
|
||||||
|
result = DiscordRoles.sanitize_role_name(role_name_input)
|
||||||
|
self.assertEqual(result, role_name_expected)
|
||||||
|
|
||||||
def test_objects_are_hashable(self):
|
def test_objects_are_hashable(self):
|
||||||
# given
|
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_b = DiscordRoles([ROLE_BRAVO, ROLE_ALPHA])
|
||||||
role_b = create_role()
|
roles_c = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE])
|
||||||
role_c = create_role()
|
|
||||||
roles_a = RolesSet([role_a, role_b])
|
|
||||||
roles_b = RolesSet([role_b, role_a])
|
|
||||||
roles_c = RolesSet([role_a, role_b, role_c])
|
|
||||||
# when/then
|
|
||||||
self.assertIsNotNone(hash(roles_a))
|
self.assertIsNotNone(hash(roles_a))
|
||||||
self.assertEqual(hash(roles_a), hash(roles_b))
|
self.assertEqual(hash(roles_a), hash(roles_b))
|
||||||
self.assertNotEqual(hash(roles_a), hash(roles_c))
|
self.assertNotEqual(hash(roles_a), hash(roles_c))
|
||||||
|
|
||||||
def test_create_from_matched_roles(self):
|
def test_create_from_matched_roles(self):
|
||||||
role_a = create_role()
|
|
||||||
role_b = create_role()
|
|
||||||
matched_roles = [
|
matched_roles = [
|
||||||
create_matched_role(role_a, True),
|
(ROLE_ALPHA, True),
|
||||||
create_matched_role(role_b, False),
|
(ROLE_BRAVO, False)
|
||||||
]
|
]
|
||||||
# when
|
roles = DiscordRoles.create_from_matched_roles(matched_roles)
|
||||||
roles = RolesSet.create_from_matched_roles(matched_roles)
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles, RolesSet([role_a, role_b]))
|
|
||||||
|
|
||||||
def test_return_role_ids_default(self):
|
|
||||||
role_a = create_role(id=1)
|
|
||||||
role_b = create_role(id=2)
|
|
||||||
roles = RolesSet([role_a, role_b])
|
|
||||||
# when/then
|
|
||||||
self.assertSetEqual(roles.ids(), {1, 2})
|
self.assertSetEqual(roles.ids(), {1, 2})
|
||||||
|
|
||||||
|
|
||||||
|
class TestIds(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||||
|
|
||||||
|
def test_return_role_ids_default(self):
|
||||||
|
result = self.all_roles.ids()
|
||||||
|
expected = {1, 2, 3, 13}
|
||||||
|
self.assertSetEqual(result, expected)
|
||||||
|
|
||||||
def test_return_role_ids_empty(self):
|
def test_return_role_ids_empty(self):
|
||||||
# given
|
roles = DiscordRoles([])
|
||||||
roles = RolesSet([])
|
|
||||||
# when/then
|
|
||||||
self.assertSetEqual(roles.ids(), set())
|
self.assertSetEqual(roles.ids(), set())
|
||||||
|
|
||||||
|
|
||||||
class TestRolesSetSubset(NoSocketsTestCase):
|
class TestSubset(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||||
|
|
||||||
def test_ids_only(self):
|
def test_ids_only(self):
|
||||||
# given
|
role_ids = {1, 3}
|
||||||
role_a = create_role(id=1)
|
roles_subset = self.all_roles.subset(role_ids)
|
||||||
role_b = create_role(id=2)
|
expected = {1, 3}
|
||||||
role_c = create_role(id=3)
|
self.assertSetEqual(roles_subset.ids(), expected)
|
||||||
roles_all = RolesSet([role_a, role_b, role_c])
|
|
||||||
# when
|
|
||||||
roles_subset = roles_all.subset({1, 3})
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles_subset, RolesSet([role_a, role_c]))
|
|
||||||
|
|
||||||
def test_ids_as_string_work_too(self):
|
def test_ids_as_string_work_too(self):
|
||||||
# given
|
role_ids = {'1', '3'}
|
||||||
role_a = create_role(id=1)
|
roles_subset = self.all_roles.subset(role_ids)
|
||||||
role_b = create_role(id=2)
|
expected = {1, 3}
|
||||||
role_c = create_role(id=3)
|
self.assertSetEqual(roles_subset.ids(), expected)
|
||||||
roles_all = RolesSet([role_a, role_b, role_c])
|
|
||||||
# when
|
|
||||||
roles_subset = roles_all.subset({"1", "3"})
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles_subset, RolesSet([role_a, role_c]))
|
|
||||||
|
|
||||||
def test_managed_only(self):
|
def test_managed_only(self):
|
||||||
# given
|
roles = self.all_roles.subset(managed_only=True)
|
||||||
role_a = create_role(id=1)
|
expected = {13}
|
||||||
role_m = create_role(id=13, managed=True)
|
self.assertSetEqual(roles.ids(), expected)
|
||||||
roles_all = RolesSet([role_a, role_m])
|
|
||||||
# when
|
|
||||||
roles_subset = roles_all.subset(managed_only=True)
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles_subset, RolesSet([role_m]))
|
|
||||||
|
|
||||||
def test_ids_and_managed_only(self):
|
def test_ids_and_managed_only(self):
|
||||||
# given
|
role_ids = {1, 3, 13}
|
||||||
role_a = create_role(id=1)
|
roles_subset = self.all_roles.subset(role_ids, managed_only=True)
|
||||||
role_b = create_role(id=2)
|
expected = {13}
|
||||||
role_m = create_role(id=13, managed=True)
|
self.assertSetEqual(roles_subset.ids(), expected)
|
||||||
roles_all = RolesSet([role_a, role_b, role_m])
|
|
||||||
# when
|
|
||||||
roles_subset = roles_all.subset({1, 13}, managed_only=True)
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles_subset, RolesSet([role_m]))
|
|
||||||
|
|
||||||
def test_ids_are_empty(self):
|
def test_ids_are_empty(self):
|
||||||
# given
|
roles = self.all_roles.subset([])
|
||||||
role_a = create_role(id=1)
|
expected = set()
|
||||||
role_b = create_role(id=2)
|
self.assertSetEqual(roles.ids(), expected)
|
||||||
roles_all = RolesSet([role_a, role_b])
|
|
||||||
roles_subset = roles_all.subset([])
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles_subset, RolesSet([]))
|
|
||||||
|
|
||||||
def test_no_parameters(self):
|
def test_no_parameters(self):
|
||||||
# given
|
roles = self.all_roles.subset()
|
||||||
role_a = create_role(id=1)
|
expected = {1, 2, 3, 13}
|
||||||
role_b = create_role(id=2)
|
self.assertSetEqual(roles.ids(), expected)
|
||||||
roles_all = RolesSet([role_a, role_b])
|
|
||||||
roles_subset = roles_all.subset()
|
|
||||||
# then
|
|
||||||
self.assertEqual(roles_subset, roles_all)
|
|
||||||
|
|
||||||
def test_should_return_role_names_only(self):
|
def test_should_return_role_names_only(self):
|
||||||
# given
|
# given
|
||||||
role_a = create_role(name="alpha")
|
all_roles = DiscordRoles([
|
||||||
role_b = create_role(name="bravo")
|
ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2
|
||||||
role_c1 = create_role(name="charlie")
|
])
|
||||||
role_c2 = create_role(name="Charlie")
|
|
||||||
roles_all = RolesSet([role_a, role_b, role_c1, role_c2])
|
|
||||||
# when
|
# when
|
||||||
roles_subset = roles_all.subset(role_names={"bravo", "charlie"})
|
roles = all_roles.subset(role_names={"bravo", "charlie"})
|
||||||
# then
|
# then
|
||||||
self.assertSetEqual(roles_subset, RolesSet([role_b, role_c1, role_c2]))
|
self.assertSetEqual(roles.ids(), {2, 3, 4})
|
||||||
|
|
||||||
|
|
||||||
class TestRolesSetHasRoles(NoSocketsTestCase):
|
class TestHasRoles(TestCase):
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
def setUp(self):
|
||||||
super().setUpClass()
|
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||||
role_a = create_role(id=1)
|
|
||||||
role_b = create_role(id=2)
|
|
||||||
role_c = create_role(id=3)
|
|
||||||
cls.all_roles = RolesSet([role_a, role_b, role_c])
|
|
||||||
|
|
||||||
def test_true_if_all_roles_exit(self):
|
def test_true_if_all_roles_exit(self):
|
||||||
self.assertTrue(self.all_roles.has_roles([1, 2]))
|
self.assertTrue(self.all_roles.has_roles([1, 2]))
|
||||||
|
|
||||||
def test_true_if_all_roles_exit_str(self):
|
def test_true_if_all_roles_exit_str(self):
|
||||||
self.assertTrue(self.all_roles.has_roles(["1", "2"]))
|
self.assertTrue(self.all_roles.has_roles(['1', '2']))
|
||||||
|
|
||||||
def test_false_if_role_does_not_exit(self):
|
def test_false_if_role_does_not_exit(self):
|
||||||
self.assertFalse(self.all_roles.has_roles([99]))
|
self.assertFalse(self.all_roles.has_roles([99]))
|
||||||
@@ -207,104 +183,74 @@ class TestRolesSetHasRoles(NoSocketsTestCase):
|
|||||||
self.assertTrue(self.all_roles.has_roles([]))
|
self.assertTrue(self.all_roles.has_roles([]))
|
||||||
|
|
||||||
|
|
||||||
class TestRolesSetGetMatchingRolesByName(NoSocketsTestCase):
|
class TestGetMatchingRolesByName(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||||
|
|
||||||
def test_return_role_if_matches(self):
|
def test_return_role_if_matches(self):
|
||||||
# given
|
role_name = 'alpha'
|
||||||
role_a = create_role(name="alpha")
|
expected = ROLE_ALPHA
|
||||||
role_b = create_role(name="bravo")
|
result = self.all_roles.role_by_name(role_name)
|
||||||
roles = RolesSet([role_a, role_b])
|
self.assertEqual(result, expected)
|
||||||
# when
|
|
||||||
result = roles.role_by_name("alpha")
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, role_a)
|
|
||||||
|
|
||||||
def test_return_role_if_matches_and_limit_max_length(self):
|
def test_return_role_if_matches_and_limit_max_length(self):
|
||||||
# given
|
role_name = 'x' * 120
|
||||||
role_name = "x" * 120
|
expected = create_role(77, 'x' * 100)
|
||||||
role = create_role(name="x" * 100)
|
roles = DiscordRoles([expected])
|
||||||
roles = RolesSet([role])
|
|
||||||
# when
|
|
||||||
result = roles.role_by_name(role_name)
|
result = roles.role_by_name(role_name)
|
||||||
# then
|
self.assertEqual(result, expected)
|
||||||
self.assertEqual(result, role)
|
|
||||||
|
|
||||||
def test_return_empty_if_not_matches(self):
|
def test_return_empty_if_not_matches(self):
|
||||||
# given
|
role_name = 'lima'
|
||||||
role_a = create_role(name="alpha")
|
expected = {}
|
||||||
role_b = create_role(name="bravo")
|
result = self.all_roles.role_by_name(role_name)
|
||||||
roles = RolesSet([role_a, role_b])
|
self.assertEqual(result, expected)
|
||||||
# when
|
|
||||||
result = roles.role_by_name("unknown")
|
|
||||||
# then
|
|
||||||
self.assertIsNone(result)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRolesSetUnion(NoSocketsTestCase):
|
class TestUnion(TestCase):
|
||||||
|
|
||||||
def test_distinct_sets(self):
|
def test_distinct_sets(self):
|
||||||
# given
|
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_2 = DiscordRoles([ROLE_CHARLIE, ROLE_MIKE])
|
||||||
role_b = create_role()
|
roles_3 = roles_1.union(roles_2)
|
||||||
roles_1 = RolesSet([role_a])
|
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE])
|
||||||
roles_2 = RolesSet([role_b])
|
self.assertEqual(roles_3, expected)
|
||||||
# when
|
|
||||||
result = roles_1.union(roles_2)
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, RolesSet([role_a, role_b]))
|
|
||||||
|
|
||||||
def test_overlapping_sets(self):
|
def test_overlapping_sets(self):
|
||||||
# given
|
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_2 = DiscordRoles([ROLE_BRAVO, ROLE_MIKE])
|
||||||
role_b = create_role()
|
roles_3 = roles_1.union(roles_2)
|
||||||
role_c = create_role()
|
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE])
|
||||||
roles_1 = RolesSet([role_a, role_b])
|
self.assertEqual(roles_3, expected)
|
||||||
roles_2 = RolesSet([role_b, role_c])
|
|
||||||
# when
|
|
||||||
result = roles_1.union(roles_2)
|
|
||||||
self.assertEqual(result, RolesSet([role_a, role_b, role_c]))
|
|
||||||
|
|
||||||
def test_identical_sets(self):
|
def test_identical_sets(self):
|
||||||
role_a = create_role()
|
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_b = create_role()
|
roles_2 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
roles_1 = RolesSet([role_a, role_b])
|
roles_3 = roles_1.union(roles_2)
|
||||||
roles_2 = RolesSet([role_a, role_b])
|
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
# when
|
self.assertEqual(roles_3, expected)
|
||||||
result = roles_1.union(roles_2)
|
|
||||||
self.assertEqual(result, RolesSet([role_a, role_b]))
|
|
||||||
|
|
||||||
|
|
||||||
class TestRolesSetDifference(NoSocketsTestCase):
|
class TestDifference(TestCase):
|
||||||
|
|
||||||
def test_distinct_sets(self):
|
def test_distinct_sets(self):
|
||||||
# given
|
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_2 = DiscordRoles([ROLE_CHARLIE, ROLE_MIKE])
|
||||||
role_b = create_role()
|
roles_3 = roles_1.difference(roles_2)
|
||||||
role_c = create_role()
|
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_d = create_role()
|
self.assertEqual(roles_3, expected)
|
||||||
roles_1 = RolesSet([role_a, role_b])
|
|
||||||
roles_2 = RolesSet([role_c, role_d])
|
|
||||||
# when
|
|
||||||
result = roles_1.difference(roles_2)
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, RolesSet([role_a, role_b]))
|
|
||||||
|
|
||||||
def test_overlapping_sets(self):
|
def test_overlapping_sets(self):
|
||||||
# given
|
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_2 = DiscordRoles([ROLE_BRAVO, ROLE_MIKE])
|
||||||
role_b = create_role()
|
roles_3 = roles_1.difference(roles_2)
|
||||||
role_c = create_role()
|
expected = DiscordRoles([ROLE_ALPHA])
|
||||||
roles_1 = RolesSet([role_a, role_b])
|
self.assertEqual(roles_3, expected)
|
||||||
roles_2 = RolesSet([role_b, role_c])
|
|
||||||
# when
|
|
||||||
result = roles_1.difference(roles_2)
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, RolesSet([role_a]))
|
|
||||||
|
|
||||||
def test_identical_sets(self):
|
def test_identical_sets(self):
|
||||||
# given
|
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_a = create_role()
|
roles_2 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||||
role_b = create_role()
|
roles_3 = roles_1.difference(roles_2)
|
||||||
roles_1 = RolesSet([role_a, role_b])
|
expected = DiscordRoles([])
|
||||||
roles_2 = RolesSet([role_a, role_b])
|
self.assertEqual(roles_3, expected)
|
||||||
# when
|
|
||||||
result = roles_1.difference(roles_2)
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, RolesSet([]))
|
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from ..models import Guild, GuildMember, Role, User
|
|
||||||
from .factories import create_guild, create_guild_member, create_role, create_user
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_example_objects() -> dict:
|
|
||||||
path = Path(__file__).parent / "example_objects.json"
|
|
||||||
with path.open("r", encoding="utf-8") as fp:
|
|
||||||
return json.load(fp)
|
|
||||||
|
|
||||||
|
|
||||||
class TestUser(TestCase):
|
|
||||||
def test_should_create_new_object(self):
|
|
||||||
# when
|
|
||||||
obj = User(id="42", username=123, discriminator=456)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 42)
|
|
||||||
self.assertEqual(obj.username, "123")
|
|
||||||
self.assertTrue(obj.discriminator, "456")
|
|
||||||
|
|
||||||
def test_should_create_from_dict(self):
|
|
||||||
# given
|
|
||||||
data = example_objects["users"]["80351110224678912"]
|
|
||||||
# when
|
|
||||||
obj = User.from_dict(data)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 80351110224678912)
|
|
||||||
self.assertEqual(obj.username, "Nelly")
|
|
||||||
self.assertEqual(obj.discriminator, "1337")
|
|
||||||
|
|
||||||
|
|
||||||
class TestRole(TestCase):
|
|
||||||
def test_should_create_new_object_with_defaults(self):
|
|
||||||
# when
|
|
||||||
obj = Role(id="42", name="x" * 110)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 42)
|
|
||||||
self.assertEqual(obj.name, "x" * 100)
|
|
||||||
self.assertFalse(obj.managed)
|
|
||||||
|
|
||||||
def test_should_create_new_object(self):
|
|
||||||
# when
|
|
||||||
obj = Role(id=42, name="name", managed=1)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 42)
|
|
||||||
self.assertEqual(obj.name, "name")
|
|
||||||
self.assertTrue(obj.managed)
|
|
||||||
|
|
||||||
def test_should_create_from_dict(self):
|
|
||||||
# given
|
|
||||||
data = example_objects["roles"]["41771983423143936"]
|
|
||||||
# when
|
|
||||||
obj = Role.from_dict(data)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 41771983423143936)
|
|
||||||
self.assertEqual(obj.name, "WE DEM BOYZZ!!!!!!")
|
|
||||||
self.assertFalse(obj.managed)
|
|
||||||
|
|
||||||
def test_should_convert_to_dict(self):
|
|
||||||
# given
|
|
||||||
role = create_role(id=42, name="Special Name", managed=True)
|
|
||||||
# when/then
|
|
||||||
self.assertDictEqual(
|
|
||||||
role.asdict(), {"id": 42, "name": "Special Name", "managed": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_sanitize_role_name(self):
|
|
||||||
# given
|
|
||||||
role_name_input = "x" * 110
|
|
||||||
role_name_expected = "x" * 100
|
|
||||||
# when
|
|
||||||
result = Role.sanitize_name(role_name_input)
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, role_name_expected)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGuild(TestCase):
|
|
||||||
def test_should_create_new_object(self):
|
|
||||||
# given
|
|
||||||
role_a = create_role()
|
|
||||||
# when
|
|
||||||
obj = Guild(id="42", name=123, roles=[role_a])
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 42)
|
|
||||||
self.assertEqual(obj.name, "123")
|
|
||||||
self.assertEqual(obj.roles, frozenset([role_a]))
|
|
||||||
|
|
||||||
def test_should_create_from_dict(self):
|
|
||||||
# given
|
|
||||||
data = example_objects["guilds"]["2909267986263572999"]
|
|
||||||
# when
|
|
||||||
obj = Guild.from_dict(data)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.id, 2909267986263572999)
|
|
||||||
self.assertEqual(obj.name, "Mason's Test Server")
|
|
||||||
(first_role,) = obj.roles
|
|
||||||
self.assertEqual(first_role.id, 2909267986263572999)
|
|
||||||
|
|
||||||
def test_should_raise_error_when_role_type_is_wrong(self):
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
create_guild(roles=[create_role(), "invalid"])
|
|
||||||
|
|
||||||
|
|
||||||
class TestGuildMember(TestCase):
|
|
||||||
def test_should_create_new_object(self):
|
|
||||||
# given
|
|
||||||
user = create_user()
|
|
||||||
# when
|
|
||||||
obj = GuildMember(user=user, nick="x" * 40, roles=[1, 2])
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.user, user)
|
|
||||||
self.assertEqual(obj.nick, "x" * 32)
|
|
||||||
self.assertEqual(obj.roles, frozenset([1, 2]))
|
|
||||||
|
|
||||||
def test_should_create_from_dict_empty(self):
|
|
||||||
# given
|
|
||||||
data = example_objects["guildMembers"]["1"]
|
|
||||||
# when
|
|
||||||
obj = GuildMember.from_dict(data)
|
|
||||||
# then
|
|
||||||
self.assertIsNone(obj.user)
|
|
||||||
self.assertSetEqual(obj.roles, set())
|
|
||||||
self.assertIsNone(obj.nick)
|
|
||||||
|
|
||||||
def test_should_create_from_dict_full(self):
|
|
||||||
# given
|
|
||||||
data = example_objects["guildMembers"]["2"]
|
|
||||||
# when
|
|
||||||
obj = GuildMember.from_dict(data)
|
|
||||||
# then
|
|
||||||
self.assertEqual(obj.user.username, "Nelly")
|
|
||||||
self.assertSetEqual(obj.roles, {197150972374548480, 41771983423143936})
|
|
||||||
self.assertEqual(obj.nick, "Nelly the great")
|
|
||||||
|
|
||||||
def test_should_raise_error_when_user_type_is_wrong(self):
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
create_guild_member(user="invalid")
|
|
||||||
|
|
||||||
def test_should_raise_error_when_role_type_is_wrong(self):
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
GuildMember(roles=[1, 2, "invalid"])
|
|
||||||
|
|
||||||
def test_sanitize_nick(self):
|
|
||||||
# given
|
|
||||||
nick_input = "x" * 40
|
|
||||||
nick_expected = "x" * 32
|
|
||||||
# when
|
|
||||||
result = GuildMember.sanitize_nick(nick_input)
|
|
||||||
# then
|
|
||||||
self.assertEqual(result, nick_expected)
|
|
||||||
|
|
||||||
|
|
||||||
example_objects = _fetch_example_objects()
|
|
||||||
@@ -1,33 +1,30 @@
|
|||||||
import logging
|
import logging
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from requests.exceptions import HTTPError
|
|
||||||
from requests_oauthlib import OAuth2Session
|
from requests_oauthlib import OAuth2Session
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import User, Group
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from allianceauth.services.hooks import NameFormatter
|
||||||
|
|
||||||
from . import __title__
|
from . import __title__
|
||||||
from .app_settings import (
|
from .app_settings import (
|
||||||
DISCORD_APP_ID,
|
DISCORD_APP_ID,
|
||||||
DISCORD_APP_SECRET,
|
DISCORD_APP_SECRET,
|
||||||
|
DISCORD_BOT_TOKEN,
|
||||||
DISCORD_CALLBACK_URL,
|
DISCORD_CALLBACK_URL,
|
||||||
DISCORD_GUILD_ID,
|
DISCORD_GUILD_ID,
|
||||||
DISCORD_SYNC_NAMES,
|
DISCORD_SYNC_NAMES
|
||||||
)
|
|
||||||
from .core import calculate_roles_for_user, create_bot_client
|
|
||||||
from .core import group_to_role as core_group_to_role
|
|
||||||
from .core import server_name as core_server_name
|
|
||||||
from .core import user_formatted_nick
|
|
||||||
from .discord_client import (
|
|
||||||
DISCORD_OAUTH_BASE_URL,
|
|
||||||
DISCORD_OAUTH_TOKEN_URL,
|
|
||||||
DiscordApiBackoff,
|
|
||||||
DiscordClient,
|
|
||||||
)
|
)
|
||||||
|
from .discord_client import DiscordClient
|
||||||
|
from .discord_client.exceptions import DiscordClientException, DiscordApiBackoff
|
||||||
|
from .discord_client.helpers import match_or_create_roles_from_names
|
||||||
from .utils import LoggerAddTag
|
from .utils import LoggerAddTag
|
||||||
|
|
||||||
|
|
||||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||||
|
|
||||||
|
|
||||||
@@ -59,68 +56,79 @@ class DiscordUserManager(models.Manager):
|
|||||||
Returns: True on success, else False or raises exception
|
Returns: True on success, else False or raises exception
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
nickname = user_formatted_nick(user) if DISCORD_SYNC_NAMES else None
|
nickname = self.user_formatted_nick(user) if DISCORD_SYNC_NAMES else None
|
||||||
|
group_names = self.user_group_names(user)
|
||||||
access_token = self._exchange_auth_code_for_token(authorization_code)
|
access_token = self._exchange_auth_code_for_token(authorization_code)
|
||||||
user_client = DiscordClient(access_token, is_rate_limited=is_rate_limited)
|
user_client = DiscordClient(access_token, is_rate_limited=is_rate_limited)
|
||||||
discord_user = user_client.current_user()
|
discord_user = user_client.current_user()
|
||||||
bot_client = create_bot_client(is_rate_limited=is_rate_limited)
|
user_id = discord_user['id']
|
||||||
roles, changed = calculate_roles_for_user(
|
bot_client = self._bot_client(is_rate_limited=is_rate_limited)
|
||||||
user=user, client=bot_client, discord_uid=discord_user.id
|
|
||||||
)
|
|
||||||
if changed is None:
|
|
||||||
# Handle new member
|
|
||||||
created = bot_client.add_guild_member(
|
|
||||||
guild_id=DISCORD_GUILD_ID,
|
|
||||||
user_id=discord_user.id,
|
|
||||||
access_token=access_token,
|
|
||||||
role_ids=list(roles.ids()),
|
|
||||||
nick=nickname
|
|
||||||
)
|
|
||||||
if not created:
|
|
||||||
logger.warning(
|
|
||||||
"Failed to add user %s with Discord ID %s to Discord server",
|
|
||||||
user,
|
|
||||||
discord_user.id,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# Handle existing member
|
|
||||||
logger.debug(
|
|
||||||
"User %s with Discord ID %s is already a member. Forcing a Refresh",
|
|
||||||
user,
|
|
||||||
discord_user.id,
|
|
||||||
)
|
|
||||||
# Force an update cause the discord API won't do it for us.
|
|
||||||
updated = bot_client.modify_guild_member(
|
|
||||||
guild_id=DISCORD_GUILD_ID,
|
|
||||||
user_id=discord_user.id,
|
|
||||||
role_ids=list(roles.ids()),
|
|
||||||
nick=nickname
|
|
||||||
)
|
|
||||||
if not updated:
|
|
||||||
# Could not update the new user so fail.
|
|
||||||
logger.warning(
|
|
||||||
"Failed to add user %s with Discord ID %s to Discord server",
|
|
||||||
user,
|
|
||||||
discord_user.id,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.update_or_create(
|
if group_names:
|
||||||
user=user,
|
role_ids = match_or_create_roles_from_names(
|
||||||
defaults={
|
client=bot_client,
|
||||||
'uid': discord_user.id,
|
guild_id=DISCORD_GUILD_ID,
|
||||||
'username': discord_user.username[:32],
|
role_names=group_names
|
||||||
'discriminator': discord_user.discriminator[:4],
|
).ids()
|
||||||
'activated': now()
|
else:
|
||||||
}
|
role_ids = None
|
||||||
|
|
||||||
|
created = bot_client.add_guild_member(
|
||||||
|
guild_id=DISCORD_GUILD_ID,
|
||||||
|
user_id=user_id,
|
||||||
|
access_token=access_token,
|
||||||
|
role_ids=role_ids,
|
||||||
|
nick=nickname
|
||||||
)
|
)
|
||||||
logger.info(
|
if created is not False:
|
||||||
"Added user %s with Discord ID %s to Discord server",
|
if created is None:
|
||||||
user,
|
logger.debug(
|
||||||
discord_user.id
|
"User %s with Discord ID %s is already a member. Forcing a Refresh",
|
||||||
)
|
user,
|
||||||
return True
|
user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Force an update cause the discord API won't do it for us.
|
||||||
|
if role_ids:
|
||||||
|
role_ids = list(role_ids)
|
||||||
|
|
||||||
|
updated = bot_client.modify_guild_member(
|
||||||
|
guild_id=DISCORD_GUILD_ID,
|
||||||
|
user_id=user_id,
|
||||||
|
role_ids=role_ids,
|
||||||
|
nick=nickname
|
||||||
|
)
|
||||||
|
|
||||||
|
if not updated:
|
||||||
|
# Could not update the new user so fail.
|
||||||
|
logger.warning(
|
||||||
|
"Failed to add user %s with Discord ID %s to Discord server",
|
||||||
|
user,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.update_or_create(
|
||||||
|
user=user,
|
||||||
|
defaults={
|
||||||
|
'uid': user_id,
|
||||||
|
'username': discord_user['username'][:32],
|
||||||
|
'discriminator': discord_user['discriminator'][:4],
|
||||||
|
'activated': now()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Added user %s with Discord ID %s to Discord server", user, user_id
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to add user %s with Discord ID %s to Discord server",
|
||||||
|
user,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
|
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
@@ -128,6 +136,31 @@ class DiscordUserManager(models.Manager):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def user_formatted_nick(user: User) -> str:
|
||||||
|
"""returns the name of the given users main character with name formatting
|
||||||
|
or None if user has no main
|
||||||
|
"""
|
||||||
|
from .auth_hooks import DiscordService
|
||||||
|
|
||||||
|
if user.profile.main_character:
|
||||||
|
return NameFormatter(DiscordService(), user).format_name()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def user_group_names(user: User, state_name: str = None) -> list:
|
||||||
|
"""returns list of group names plus state the given user is a member of"""
|
||||||
|
if not state_name:
|
||||||
|
state_name = user.profile.state.name
|
||||||
|
group_names = (
|
||||||
|
[group.name for group in user.groups.all()] + [state_name]
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
"Group names for roles updates of user %s are: %s", user, group_names
|
||||||
|
)
|
||||||
|
return group_names
|
||||||
|
|
||||||
def user_has_account(self, user: User) -> bool:
|
def user_has_account(self, user: User) -> bool:
|
||||||
"""Returns True if the user has an Discord account, else False
|
"""Returns True if the user has an Discord account, else False
|
||||||
|
|
||||||
@@ -145,41 +178,60 @@ class DiscordUserManager(models.Manager):
|
|||||||
'permissions': str(cls.BOT_PERMISSIONS)
|
'permissions': str(cls.BOT_PERMISSIONS)
|
||||||
|
|
||||||
})
|
})
|
||||||
return f'{DISCORD_OAUTH_BASE_URL}?{params}'
|
return f'{DiscordClient.OAUTH_BASE_URL}?{params}'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_oauth_redirect_url(cls) -> str:
|
def generate_oauth_redirect_url(cls) -> str:
|
||||||
oauth = OAuth2Session(
|
oauth = OAuth2Session(
|
||||||
DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL, scope=cls.SCOPES
|
DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL, scope=cls.SCOPES
|
||||||
)
|
)
|
||||||
url, _ = oauth.authorization_url(DISCORD_OAUTH_BASE_URL)
|
url, state = oauth.authorization_url(DiscordClient.OAUTH_BASE_URL)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _exchange_auth_code_for_token(authorization_code: str) -> str:
|
def _exchange_auth_code_for_token(authorization_code: str) -> str:
|
||||||
oauth = OAuth2Session(DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL)
|
oauth = OAuth2Session(DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL)
|
||||||
token = oauth.fetch_token(
|
token = oauth.fetch_token(
|
||||||
DISCORD_OAUTH_TOKEN_URL,
|
DiscordClient.OAUTH_TOKEN_URL,
|
||||||
client_secret=DISCORD_APP_SECRET,
|
client_secret=DISCORD_APP_SECRET,
|
||||||
code=authorization_code
|
code=authorization_code
|
||||||
)
|
)
|
||||||
logger.debug("Received token from OAuth")
|
logger.debug("Received token from OAuth")
|
||||||
return token['access_token']
|
return token['access_token']
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def group_to_role(group: Group) -> dict:
|
def server_name(cls, use_cache: bool = True) -> str:
|
||||||
"""Fetch the Discord role matching the given Django group by name.
|
"""returns the name of the current Discord server
|
||||||
|
or an empty string if the name could not be retrieved
|
||||||
|
|
||||||
Returns:
|
Params:
|
||||||
- Discord role as dict
|
- use_cache: When set False will force an API call to get the server name
|
||||||
- empty dict if no matching role found
|
|
||||||
"""
|
"""
|
||||||
role = core_group_to_role(group)
|
try:
|
||||||
return role.asdict() if role else dict()
|
server_name = cls._bot_client().guild_name(
|
||||||
|
guild_id=DISCORD_GUILD_ID, use_cache=use_cache
|
||||||
|
)
|
||||||
|
except (HTTPError, DiscordClientException):
|
||||||
|
server_name = ""
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Unexpected error when trying to retrieve the server name from Discord",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
server_name = ""
|
||||||
|
|
||||||
|
return server_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def group_to_role(cls, group: Group) -> dict:
|
||||||
|
"""returns the Discord role matching the given Django group by name
|
||||||
|
or an empty dict() if no matching role exist
|
||||||
|
"""
|
||||||
|
return cls._bot_client().match_role_from_name(
|
||||||
|
guild_id=DISCORD_GUILD_ID, role_name=group.name
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def server_name(use_cache: bool = True) -> str:
|
def _bot_client(is_rate_limited: bool = True) -> DiscordClient:
|
||||||
"""Fetches the name of the current Discord server.
|
"""returns a bot client for access to the Discord API"""
|
||||||
This method is kept to ensure backwards compatibility of this API.
|
return DiscordClient(DISCORD_BOT_TOKEN, is_rate_limited=is_rate_limited)
|
||||||
"""
|
|
||||||
return core_server_name(use_cache)
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
@@ -7,17 +6,13 @@ 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 .core import (
|
from .discord_client import DiscordApiBackoff, DiscordClient, DiscordRoles
|
||||||
create_bot_client,
|
from .discord_client.helpers import match_or_create_roles_from_names
|
||||||
default_bot_client,
|
|
||||||
calculate_roles_for_user,
|
|
||||||
user_formatted_nick
|
|
||||||
)
|
|
||||||
from .discord_client import DiscordApiBackoff
|
|
||||||
from .managers import DiscordUserManager
|
from .managers import DiscordUserManager
|
||||||
from .utils import LoggerAddTag
|
from .utils import LoggerAddTag
|
||||||
|
|
||||||
@@ -26,13 +21,14 @@ logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
|||||||
|
|
||||||
|
|
||||||
class DiscordUser(models.Model):
|
class DiscordUser(models.Model):
|
||||||
"""The Discord user account of an Auth user."""
|
|
||||||
|
USER_RELATED_NAME = 'discord'
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
User,
|
User,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='discord',
|
related_name=USER_RELATED_NAME,
|
||||||
help_text='Auth user owning this Discord account'
|
help_text='Auth user owning this Discord account'
|
||||||
)
|
)
|
||||||
uid = models.BigIntegerField(
|
uid = models.BigIntegerField(
|
||||||
@@ -84,21 +80,24 @@ class DiscordUser(models.Model):
|
|||||||
- False on error or raises exception
|
- False on error or raises exception
|
||||||
"""
|
"""
|
||||||
if not nickname:
|
if not nickname:
|
||||||
nickname = user_formatted_nick(self.user)
|
nickname = DiscordUser.objects.user_formatted_nick(self.user)
|
||||||
if not nickname:
|
if nickname:
|
||||||
return False
|
client = DiscordUser.objects._bot_client()
|
||||||
success = default_bot_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,
|
||||||
nick=nickname
|
nick=nickname
|
||||||
)
|
)
|
||||||
if success:
|
if success:
|
||||||
logger.info('Nickname for %s has been updated', self.user)
|
logger.info('Nickname for %s has been updated', self.user)
|
||||||
else:
|
else:
|
||||||
logger.warning('Failed to update nickname for %s', self.user)
|
logger.warning('Failed to update nickname for %s', self.user)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def update_groups(self, state_name: str = None) -> Optional[bool]:
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_groups(self, state_name: str = None) -> bool:
|
||||||
"""update groups for a user based on his current group memberships.
|
"""update groups for a user based on his current group memberships.
|
||||||
Will add or remove roles of a user as needed.
|
Will add or remove roles of a user as needed.
|
||||||
|
|
||||||
@@ -110,18 +109,57 @@ 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
|
||||||
"""
|
"""
|
||||||
new_roles, is_changed = calculate_roles_for_user(
|
client = DiscordUser.objects._bot_client()
|
||||||
user=self.user,
|
member_roles = self._determine_member_roles(client)
|
||||||
client=default_bot_client,
|
if member_roles is None:
|
||||||
discord_uid=self.uid,
|
|
||||||
state_name=state_name
|
|
||||||
)
|
|
||||||
if is_changed is None:
|
|
||||||
logger.debug('User is not a member of this guild %s', self.user)
|
|
||||||
return None
|
return None
|
||||||
if is_changed:
|
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)
|
||||||
|
if member_info is None:
|
||||||
|
return None # User is no longer a member
|
||||||
|
guild_roles = DiscordRoles(client.guild_roles(guild_id=DISCORD_GUILD_ID))
|
||||||
|
logger.debug('Current guild roles: %s', guild_roles.ids())
|
||||||
|
if 'roles' in member_info:
|
||||||
|
if not guild_roles.has_roles(member_info['roles']):
|
||||||
|
guild_roles = DiscordRoles(
|
||||||
|
client.guild_roles(guild_id=DISCORD_GUILD_ID, use_cache=False)
|
||||||
|
)
|
||||||
|
if not guild_roles.has_roles(member_info['roles']):
|
||||||
|
raise RuntimeError(
|
||||||
|
'Member {} has unknown roles: {}'.format(
|
||||||
|
self.user,
|
||||||
|
set(member_info['roles']).difference(guild_roles.ids())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return guild_roles.subset(member_info['roles'])
|
||||||
|
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(
|
||||||
|
client=client,
|
||||||
|
guild_id=DISCORD_GUILD_ID,
|
||||||
|
role_names=DiscordUser.objects.user_group_names(
|
||||||
|
user=self.user, state_name=state_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
'Requested roles for user %s: %s', self.user, requested_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_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)
|
||||||
success = default_bot_client.modify_guild_member(
|
new_roles = requested_roles.union(member_roles_persistent)
|
||||||
|
success = client.modify_guild_member(
|
||||||
guild_id=DISCORD_GUILD_ID,
|
guild_id=DISCORD_GUILD_ID,
|
||||||
user_id=self.uid,
|
user_id=self.uid,
|
||||||
role_ids=list(new_roles.ids())
|
role_ids=list(new_roles.ids())
|
||||||
@@ -134,32 +172,41 @@ class DiscordUser(models.Model):
|
|||||||
logger.info('No need to update roles for user %s', self.user)
|
logger.info('No need to update roles for user %s', self.user)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_username(self) -> Optional[bool]:
|
def update_username(self) -> bool:
|
||||||
"""Updates the username incl. the discriminator
|
"""Updates the username incl. the discriminator
|
||||||
from the Discord server and saves it
|
from the Discord server and saves it
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- True on success
|
- True on success
|
||||||
- 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
|
||||||
"""
|
"""
|
||||||
member_info = default_bot_client.guild_member(
|
client = DiscordUser.objects._bot_client()
|
||||||
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 not member_info:
|
success = None
|
||||||
logger.warning('%s: User not a guild member', self.user)
|
elif (
|
||||||
return None
|
user_info
|
||||||
self.username = member_info.user.username
|
and 'user' in user_info
|
||||||
self.discriminator = member_info.user.discriminator
|
and 'username' in user_info['user']
|
||||||
self.save()
|
and 'discriminator' in user_info['user']
|
||||||
logger.info('%s: Username has been updated', self.user)
|
):
|
||||||
return True
|
self.username = user_info['user']['username']
|
||||||
|
self.discriminator = user_info['user']['discriminator']
|
||||||
|
self.save()
|
||||||
|
logger.info('Username for %s has been updated', self.user)
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
logger.warning('Failed to update username for %s', self.user)
|
||||||
|
success = False
|
||||||
|
return success
|
||||||
|
|
||||||
def delete_user(
|
def delete_user(
|
||||||
self,
|
self,
|
||||||
notify_user: bool = False,
|
notify_user: bool = False,
|
||||||
is_rate_limited: bool = True,
|
is_rate_limited: bool = True,
|
||||||
handle_api_exceptions: bool = False
|
handle_api_exceptions: bool = False
|
||||||
) -> Optional[bool]:
|
) -> bool:
|
||||||
"""Deletes the Discount user both on the server and locally
|
"""Deletes the Discount user both on the server and locally
|
||||||
|
|
||||||
Params:
|
Params:
|
||||||
@@ -174,7 +221,7 @@ class DiscordUser(models.Model):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_user = self.user
|
_user = self.user
|
||||||
client = create_bot_client(is_rate_limited=is_rate_limited)
|
client = DiscordUser.objects._bot_client(is_rate_limited=is_rate_limited)
|
||||||
success = client.remove_guild_member(
|
success = client.remove_guild_member(
|
||||||
guild_id=DISCORD_GUILD_ID, user_id=self.uid
|
guild_id=DISCORD_GUILD_ID, user_id=self.uid
|
||||||
)
|
)
|
||||||
@@ -194,13 +241,15 @@ class DiscordUser(models.Model):
|
|||||||
)
|
)
|
||||||
logger.info('Account for user %s was deleted.', _user)
|
logger.info('Account for user %s was deleted.', _user)
|
||||||
return True
|
return True
|
||||||
logger.debug('Account for user %s was already deleted.', _user)
|
else:
|
||||||
return None
|
logger.debug('Account for user %s was already deleted.', _user)
|
||||||
|
return None
|
||||||
|
|
||||||
logger.warning(
|
else:
|
||||||
'Failed to remove user %s from the Discord server', _user
|
logger.warning(
|
||||||
)
|
'Failed to remove user %s from the Discord server', _user
|
||||||
return False
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
|
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
|
||||||
if handle_api_exceptions:
|
if handle_api_exceptions:
|
||||||
@@ -208,4 +257,5 @@ class DiscordUser(models.Model):
|
|||||||
'Failed to remove user %s from Discord server: %s',self.user, ex
|
'Failed to remove user %s from Discord server: %s',self.user, ex
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
raise ex
|
else:
|
||||||
|
raise ex
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
from ..discord_client.tests import ( # noqa
|
||||||
|
TEST_GUILD_ID,
|
||||||
|
TEST_USER_ID,
|
||||||
|
TEST_USER_NAME,
|
||||||
|
TEST_USER_DISCRIMINATOR,
|
||||||
|
create_role,
|
||||||
|
ROLE_ALPHA,
|
||||||
|
ROLE_BRAVO,
|
||||||
|
ROLE_CHARLIE,
|
||||||
|
ROLE_CHARLIE_2,
|
||||||
|
ROLE_MIKE,
|
||||||
|
ALL_ROLES,
|
||||||
|
create_user_info
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_AUTH_GROUP = 'Member'
|
DEFAULT_AUTH_GROUP = 'Member'
|
||||||
MODULE_PATH = 'allianceauth.services.modules.discord'
|
MODULE_PATH = 'allianceauth.services.modules.discord'
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
from django.utils.timezone import now
|
|
||||||
|
|
||||||
from allianceauth.authentication.backends import StateBackend
|
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
|
||||||
|
|
||||||
from ..discord_client.tests.factories import (
|
|
||||||
TEST_USER_DISCRIMINATOR,
|
|
||||||
TEST_USER_ID,
|
|
||||||
TEST_USER_NAME,
|
|
||||||
)
|
|
||||||
from ..models import DiscordUser
|
|
||||||
|
|
||||||
|
|
||||||
def create_user(**kwargs):
|
|
||||||
params = {"username": TEST_USER_NAME}
|
|
||||||
params.update(kwargs)
|
|
||||||
username = StateBackend.iterate_username(params["username"])
|
|
||||||
user = AuthUtils.create_user(username)
|
|
||||||
return AuthUtils.add_permission_to_user_by_name("discord.access_discord", user)
|
|
||||||
|
|
||||||
|
|
||||||
def create_discord_user(user=None, **kwargs):
|
|
||||||
params = {
|
|
||||||
"user": user or create_user(),
|
|
||||||
"uid": TEST_USER_ID,
|
|
||||||
"username": TEST_USER_NAME,
|
|
||||||
"discriminator": TEST_USER_DISCRIMINATOR,
|
|
||||||
"activated": now(),
|
|
||||||
}
|
|
||||||
params.update(kwargs)
|
|
||||||
return DiscordUser.objects.create(**params)
|
|
||||||
@@ -35,17 +35,17 @@ import logging
|
|||||||
from uuid import uuid1
|
from uuid import uuid1
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from django.core.cache import caches
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
|
|
||||||
from allianceauth.services.modules.discord.models import DiscordUser
|
from allianceauth.services.modules.discord.models import DiscordUser
|
||||||
from allianceauth.utils.cache import get_redis_client
|
|
||||||
|
|
||||||
logger = logging.getLogger('allianceauth')
|
logger = logging.getLogger('allianceauth')
|
||||||
MAX_RUNS = 3
|
MAX_RUNS = 3
|
||||||
|
|
||||||
|
|
||||||
def clear_cache():
|
def clear_cache():
|
||||||
redis = get_redis_client()
|
default_cache = caches['default']
|
||||||
|
redis = default_cache.get_master_client()
|
||||||
redis.flushall()
|
redis.flushall()
|
||||||
logger.info('Cache flushed')
|
logger.info('Cache flushed')
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
from unittest.mock import patch
|
from django.test import TestCase, RequestFactory
|
||||||
|
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import RequestFactory
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from allianceauth.authentication.models import CharacterOwnership
|
from allianceauth.authentication.models import CharacterOwnership
|
||||||
from allianceauth.eveonline.models import (
|
from allianceauth.eveonline.models import (
|
||||||
EveAllianceInfo,
|
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||||
EveCharacter,
|
|
||||||
EveCorporationInfo,
|
|
||||||
)
|
)
|
||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
|
||||||
|
|
||||||
from ....admin import (
|
from ....admin import (
|
||||||
MainAllianceFilter,
|
|
||||||
MainCorporationsFilter,
|
|
||||||
ServicesUserAdmin,
|
|
||||||
user_main_organization,
|
|
||||||
user_profile_pic,
|
user_profile_pic,
|
||||||
user_username,
|
user_username,
|
||||||
|
user_main_organization,
|
||||||
|
ServicesUserAdmin,
|
||||||
|
MainCorporationsFilter,
|
||||||
|
MainAllianceFilter
|
||||||
)
|
)
|
||||||
from ..admin import DiscordUserAdmin
|
from ..admin import DiscordUserAdmin
|
||||||
from ..models import DiscordUser
|
from ..models import DiscordUser
|
||||||
from . import MODULE_PATH
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataMixin(NoSocketsTestCase):
|
class TestDataMixin(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
@@ -174,7 +168,7 @@ class TestDataMixin(NoSocketsTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestColumnRendering(TestDataMixin, NoSocketsTestCase):
|
class TestColumnRendering(TestDataMixin, TestCase):
|
||||||
|
|
||||||
def test_user_profile_pic_u1(self):
|
def test_user_profile_pic_u1(self):
|
||||||
expected = (
|
expected = (
|
||||||
@@ -235,7 +229,7 @@ class TestColumnRendering(TestDataMixin, NoSocketsTestCase):
|
|||||||
# actions
|
# actions
|
||||||
|
|
||||||
|
|
||||||
class TestFilters(TestDataMixin, NoSocketsTestCase):
|
class TestFilters(TestDataMixin, TestCase):
|
||||||
|
|
||||||
def test_filter_main_corporations(self):
|
def test_filter_main_corporations(self):
|
||||||
|
|
||||||
@@ -293,16 +287,3 @@ class TestFilters(TestDataMixin, NoSocketsTestCase):
|
|||||||
queryset = changelist.get_queryset(request)
|
queryset = changelist.get_queryset(request)
|
||||||
expected = [self.user_1.discord]
|
expected = [self.user_1.discord]
|
||||||
self.assertSetEqual(set(queryset), set(expected))
|
self.assertSetEqual(set(queryset), set(expected))
|
||||||
|
|
||||||
|
|
||||||
@patch(MODULE_PATH + ".admin.DiscordUser.delete_user")
|
|
||||||
class TestDeleteQueryset(TestDataMixin, NoSocketsTestCase):
|
|
||||||
def test_should_delete_all_objects(self, mock_delete_user):
|
|
||||||
# given
|
|
||||||
request = self.factory.get('/')
|
|
||||||
request.user = self.user_1
|
|
||||||
queryset = DiscordUser.objects.filter(user__in=[self.user_2, self.user_3])
|
|
||||||
# when
|
|
||||||
self.modeladmin.delete_queryset(request, queryset)
|
|
||||||
# then
|
|
||||||
self.assertEqual(mock_delete_user.call_count, 2)
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
|
||||||
|
|
||||||
from ..api import discord_guild_id
|
|
||||||
from . import MODULE_PATH
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiscordGuildId(NoSocketsTestCase):
|
|
||||||
@patch(MODULE_PATH + ".api.DISCORD_GUILD_ID", "123")
|
|
||||||
def test_should_return_guild_id_when_configured(self):
|
|
||||||
self.assertEqual(discord_guild_id(), 123)
|
|
||||||
|
|
||||||
@patch(MODULE_PATH + ".api.DISCORD_GUILD_ID", "")
|
|
||||||
def test_should_return_none_when_not_configured(self):
|
|
||||||
self.assertIsNone(discord_guild_id())
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
from allianceauth.notifications.models import Notification
|
from allianceauth.notifications.models import Notification
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
|
||||||
|
|
||||||
|
from . import TEST_USER_NAME, TEST_USER_ID, add_permissions_to_members, MODULE_PATH
|
||||||
from ..auth_hooks import DiscordService
|
from ..auth_hooks import DiscordService
|
||||||
from ..discord_client.tests.factories import TEST_USER_ID, TEST_USER_NAME
|
from ..discord_client import DiscordClient
|
||||||
from ..models import DiscordUser
|
from ..models import DiscordUser
|
||||||
from ..utils import set_logger_to_file
|
from ..utils import set_logger_to_file
|
||||||
from . import MODULE_PATH, add_permissions_to_members
|
|
||||||
|
|
||||||
logger = set_logger_to_file(MODULE_PATH + '.auth_hooks', __file__)
|
logger = set_logger_to_file(MODULE_PATH + '.auth_hooks', __file__)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
class TestDiscordService(NoSocketsTestCase):
|
class TestDiscordService(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.member = AuthUtils.create_member(TEST_USER_NAME)
|
self.member = AuthUtils.create_member(TEST_USER_NAME)
|
||||||
@@ -64,11 +64,11 @@ class TestDiscordService(NoSocketsTestCase):
|
|||||||
|
|
||||||
@patch(MODULE_PATH + '.models.notify')
|
@patch(MODULE_PATH + '.models.notify')
|
||||||
@patch(MODULE_PATH + '.tasks.DiscordUser')
|
@patch(MODULE_PATH + '.tasks.DiscordUser')
|
||||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||||
def test_validate_user(
|
def test_validate_user(
|
||||||
self, mock_create_bot_client, mock_DiscordUser, mock_notify
|
self, mock_DiscordClient, mock_DiscordUser, mock_notify
|
||||||
):
|
):
|
||||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||||
|
|
||||||
# Test member is not deleted
|
# Test member is not deleted
|
||||||
service = self.service()
|
service = self.service()
|
||||||
@@ -92,38 +92,33 @@ class TestDiscordService(NoSocketsTestCase):
|
|||||||
service.sync_nicknames_bulk([self.member])
|
service.sync_nicknames_bulk([self.member])
|
||||||
self.assertTrue(mock_update_nicknames_bulk.delay.called)
|
self.assertTrue(mock_update_nicknames_bulk.delay.called)
|
||||||
|
|
||||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||||
def test_delete_user_is_member(self, mock_create_bot_client):
|
def test_delete_user_is_member(self, mock_DiscordClient):
|
||||||
# given
|
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
|
||||||
service = self.service()
|
service = self.service()
|
||||||
# when
|
|
||||||
service.delete_user(self.member, notify_user=True)
|
service.delete_user(self.member, notify_user=True)
|
||||||
# then
|
|
||||||
self.assertTrue(mock_create_bot_client.return_value.remove_guild_member.called)
|
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||||
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
|
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
|
||||||
self.assertTrue(Notification.objects.filter(user=self.member).exists())
|
self.assertTrue(Notification.objects.filter(user=self.member).exists())
|
||||||
|
|
||||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||||
def test_delete_user_is_not_member(self, mock_create_bot_client):
|
def test_delete_user_is_not_member(self, mock_DiscordClient):
|
||||||
# given
|
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
|
||||||
service = self.service()
|
|
||||||
# when
|
|
||||||
service.delete_user(self.none_member)
|
|
||||||
# then
|
|
||||||
self.assertFalse(mock_create_bot_client.return_value.remove_guild_member.called)
|
|
||||||
|
|
||||||
@patch(MODULE_PATH + '.auth_hooks.server_name')
|
service = self.service()
|
||||||
def test_render_services_ctrl_with_username(self, mock_server_name):
|
service.delete_user(self.none_member)
|
||||||
# given
|
|
||||||
mock_server_name.return_value = "My server"
|
self.assertFalse(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||||
|
def test_render_services_ctrl_with_username(self, mock_DiscordClient):
|
||||||
service = self.service()
|
service = self.service()
|
||||||
request = self.factory.get('/services/')
|
request = self.factory.get('/services/')
|
||||||
request.user = self.member
|
request.user = self.member
|
||||||
# when
|
|
||||||
response = service.render_services_ctrl(request)
|
response = service.render_services_ctrl(request)
|
||||||
# then
|
|
||||||
self.assertTemplateUsed(service.service_ctrl_template)
|
self.assertTemplateUsed(service.service_ctrl_template)
|
||||||
self.assertIn('/discord/reset/', response)
|
self.assertIn('/discord/reset/', response)
|
||||||
self.assertIn('/discord/deactivate/', response)
|
self.assertIn('/discord/deactivate/', response)
|
||||||
@@ -135,18 +130,15 @@ class TestDiscordService(NoSocketsTestCase):
|
|||||||
response = service.render_services_ctrl(request)
|
response = service.render_services_ctrl(request)
|
||||||
self.assertIn('/discord/activate/', response)
|
self.assertIn('/discord/activate/', response)
|
||||||
|
|
||||||
@patch(MODULE_PATH + '.auth_hooks.server_name')
|
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||||
def test_render_services_ctrl_wo_username(self, mock_server_name):
|
def test_render_services_ctrl_wo_username(self, mock_DiscordClient):
|
||||||
# given
|
|
||||||
mock_server_name.return_value = "My server"
|
|
||||||
my_member = AuthUtils.create_member('John Doe')
|
my_member = AuthUtils.create_member('John Doe')
|
||||||
DiscordUser.objects.create(user=my_member, uid=111222333)
|
DiscordUser.objects.create(user=my_member, uid=111222333)
|
||||||
service = self.service()
|
service = self.service()
|
||||||
request = self.factory.get('/services/')
|
request = self.factory.get('/services/')
|
||||||
request.user = my_member
|
request.user = my_member
|
||||||
# when
|
|
||||||
response = service.render_services_ctrl(request)
|
response = service.render_services_ctrl(request)
|
||||||
# then
|
|
||||||
self.assertTemplateUsed(service.service_ctrl_template)
|
self.assertTemplateUsed(service.service_ctrl_template)
|
||||||
self.assertIn('/discord/reset/', response)
|
self.assertIn('/discord/reset/', response)
|
||||||
self.assertIn('/discord/deactivate/', response)
|
self.assertIn('/discord/deactivate/', response)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user